优化仪表盘等
This commit is contained in:
parent
db16ee70de
commit
117e2b3440
@ -1,5 +1,10 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务列表
|
||||||
|
* @param {Object} params 查询参数
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
export function listTasks(params) {
|
export function listTasks(params) {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/oa/tasks',
|
url: '/api/oa/tasks',
|
||||||
@ -8,6 +13,11 @@ export function listTasks(params) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务详情
|
||||||
|
* @param {number|string} id 任务ID
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
export function getTask(id) {
|
export function getTask(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/api/oa/tasks/${id}`,
|
url: `/api/oa/tasks/${id}`,
|
||||||
@ -15,6 +25,11 @@ export function getTask(id) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建任务
|
||||||
|
* @param {Object} data 任务数据
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
export function createTask(data) {
|
export function createTask(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/oa/tasks',
|
url: '/api/oa/tasks',
|
||||||
@ -23,6 +38,12 @@ export function createTask(data) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新任务
|
||||||
|
* @param {number|string} id 任务ID
|
||||||
|
* @param {Object} data 更新的数据
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
export function updateTask(id, data) {
|
export function updateTask(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/api/oa/tasks/${id}`,
|
url: `/api/oa/tasks/${id}`,
|
||||||
@ -31,9 +52,30 @@ export function updateTask(id, data) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除任务
|
||||||
|
* @param {number|string} id 任务ID
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
export function deleteTask(id) {
|
export function deleteTask(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/api/oa/tasks/${id}`,
|
url: `/api/oa/tasks/${id}`,
|
||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待办任务列表
|
||||||
|
* @param {Object} params
|
||||||
|
* @param {number} params.tenantId 租户ID
|
||||||
|
* @param {number} params.id 用户ID
|
||||||
|
* @param {number} [params.limit] 限制条数
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
export function getTodoTasks(params = {}) {
|
||||||
|
return request({
|
||||||
|
url: '/api/oa/tasks/todo',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -5,36 +5,22 @@
|
|||||||
<i class="fa fa-bars"></i>
|
<i class="fa fa-bars"></i>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-breadcrumb separator="/" class="bread">
|
<el-breadcrumb separator="/" class="bread">
|
||||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
<el-breadcrumb-item :to="{ path: '/' }" style="position: relative !important; top: 1px !important;">首页</el-breadcrumb-item>
|
||||||
<el-breadcrumb-item
|
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="index"
|
||||||
v-for="(item, index) in breadcrumbs"
|
:to="(item.label === '首页' || index === breadcrumbs.length - 1) ? { path: item.path } : null" style="position: relative !important; top: 1px !important;">
|
||||||
:key="index"
|
|
||||||
:to="(item.label === '首页' || index === breadcrumbs.length - 1) ? { path: item.path } : null"
|
|
||||||
>
|
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</el-breadcrumb-item>
|
</el-breadcrumb-item>
|
||||||
</el-breadcrumb>
|
</el-breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
<div class="r-content">
|
<div class="r-content">
|
||||||
<!-- 更新缓存按钮 -->
|
<!-- 更新缓存按钮 -->
|
||||||
<el-button
|
<el-button circle :icon="Refresh" @click="refreshCache" class="refresh-cache-btn" :loading="cacheLoading"
|
||||||
circle
|
title="更新菜单缓存" />
|
||||||
:icon="Refresh"
|
|
||||||
@click="refreshCache"
|
|
||||||
class="refresh-cache-btn"
|
|
||||||
:loading="cacheLoading"
|
|
||||||
title="更新菜单缓存"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 主题切换按钮 -->
|
<!-- 主题切换按钮 -->
|
||||||
<el-button
|
<el-button circle :icon="themeIcon" @click="toggleTheme" class="theme-toggle-btn"
|
||||||
circle
|
:title="currentTheme === 'dark' ? '切换到亮色模式' : '切换到暗色模式'" />
|
||||||
:icon="themeIcon"
|
|
||||||
@click="toggleTheme"
|
|
||||||
class="theme-toggle-btn"
|
|
||||||
:title="currentTheme === 'dark' ? '切换到亮色模式' : '切换到暗色模式'"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-dropdown trigger="click" @command="handleCommand">
|
<el-dropdown trigger="click" @command="handleCommand">
|
||||||
<span class="el-dropdown-link" style="cursor: pointer;">
|
<span class="el-dropdown-link" style="cursor: pointer;">
|
||||||
<img :src="getImageUrl('user')" class="user" />
|
<img :src="getImageUrl('user')" class="user" />
|
||||||
@ -43,11 +29,15 @@
|
|||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item command="profile">
|
<el-dropdown-item command="profile">
|
||||||
<el-icon><User /></el-icon>
|
<el-icon>
|
||||||
|
<User />
|
||||||
|
</el-icon>
|
||||||
<span>个人中心</span>
|
<span>个人中心</span>
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item divided command="logout">
|
<el-dropdown-item divided command="logout">
|
||||||
<el-icon><SwitchButton /></el-icon>
|
<el-icon>
|
||||||
|
<SwitchButton />
|
||||||
|
</el-icon>
|
||||||
<span>退出登录</span>
|
<span>退出登录</span>
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
@ -56,13 +46,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||||
import { useRouter, useRoute } from "vue-router";
|
import { useRouter, useRoute } from "vue-router";
|
||||||
import { useAllDataStore, useMenuStore } from "@/stores";
|
import { useAllDataStore, useMenuStore } from "@/stores";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { User, SwitchButton, Sunny, Moon, Refresh } from '@element-plus/icons-vue';
|
import { User, SwitchButton, Sunny, Moon, Refresh } from '@element-plus/icons-vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@ -95,19 +86,19 @@ async function refreshCache() {
|
|||||||
cacheLoading.value = true;
|
cacheLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await menuStore.refreshMenus();
|
await menuStore.refreshMenus();
|
||||||
|
|
||||||
// 重新加载动态路由
|
// 重新加载动态路由
|
||||||
const { loadAndAddDynamicRoutes, resetDynamicRoutes } = await import('@/router/index');
|
const { loadAndAddDynamicRoutes, resetDynamicRoutes } = await import('@/router/index');
|
||||||
// 重置路由加载状态,强制重新加载
|
// 重置路由加载状态,强制重新加载
|
||||||
resetDynamicRoutes();
|
resetDynamicRoutes();
|
||||||
await loadAndAddDynamicRoutes();
|
await loadAndAddDynamicRoutes();
|
||||||
|
|
||||||
// 等待路由完全加载(给Vue Router一些时间更新路由表)
|
// 等待路由完全加载(给Vue Router一些时间更新路由表)
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
// 触发菜单刷新事件,通知CommonAside组件刷新菜单
|
// 触发菜单刷新事件,通知CommonAside组件刷新菜单
|
||||||
window.dispatchEvent(new CustomEvent('menu-cache-refreshed'));
|
window.dispatchEvent(new CustomEvent('menu-cache-refreshed'));
|
||||||
|
|
||||||
ElMessage.success('更新成功');
|
ElMessage.success('更新成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to refresh cache', error);
|
console.error('Failed to refresh cache', error);
|
||||||
@ -117,14 +108,14 @@ async function refreshCache() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadMenu);
|
onMounted(loadMenu);
|
||||||
|
|
||||||
// 根据菜单列表和当前路径计算出的面包屑导航
|
// 根据菜单列表和当前路径计算出的面包屑导航
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
let chain: Breadcrumb[] = [];
|
let chain: Breadcrumb[] = [];
|
||||||
let currentPath = route.path || '/';
|
let currentPath = route.path || '/';
|
||||||
if (currentPath === '/' || currentPath === '') {
|
if (currentPath === '/' || currentPath === '') {
|
||||||
return [{ label: '仪表盘', path: '/' }];
|
return [{ label: '仪表盘', path: '/' }];
|
||||||
}
|
}
|
||||||
let current = menuList.value.find(m => m.path === currentPath);
|
let current = menuList.value.find(m => m.path === currentPath);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
@ -141,8 +132,8 @@ const breadcrumbs = computed(() => {
|
|||||||
parentId = parent.parentId;
|
parentId = parent.parentId;
|
||||||
} else break;
|
} else break;
|
||||||
}
|
}
|
||||||
chain = chain.reverse();
|
chain = chain.reverse();
|
||||||
return chain;
|
return chain;
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = useAllDataStore();
|
const store = useAllDataStore();
|
||||||
@ -156,17 +147,17 @@ const getImageUrl = (user) => {
|
|||||||
const displayName = computed(() => {
|
const displayName = computed(() => {
|
||||||
const user = authStore.user;
|
const user = authStore.user;
|
||||||
if (!user) return '';
|
if (!user) return '';
|
||||||
|
|
||||||
// 如果是员工登录,优先显示name
|
// 如果是员工登录,优先显示name
|
||||||
if (user.type === 'employee' && user.name) {
|
if (user.type === 'employee' && user.name) {
|
||||||
return user.name;
|
return user.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是用户登录,优先显示nickname
|
// 如果是用户登录,优先显示nickname
|
||||||
if (user.nickname) {
|
if (user.nickname) {
|
||||||
return user.nickname;
|
return user.nickname;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最后显示username
|
// 最后显示username
|
||||||
return user.username || '';
|
return user.username || '';
|
||||||
});
|
});
|
||||||
@ -192,7 +183,7 @@ const handleCommand = async (command) => {
|
|||||||
sessionStorage.removeItem('tabs_list');
|
sessionStorage.removeItem('tabs_list');
|
||||||
// 清除菜单缓存
|
// 清除菜单缓存
|
||||||
menuStore.resetMenus();
|
menuStore.resetMenus();
|
||||||
|
|
||||||
// 清除所有以 menu_cache_ 开头的本地存储项
|
// 清除所有以 menu_cache_ 开头的本地存储项
|
||||||
const menuCacheKeys: string[] = [];
|
const menuCacheKeys: string[] = [];
|
||||||
// 遍历 localStorage
|
// 遍历 localStorage
|
||||||
@ -206,7 +197,7 @@ const handleCommand = async (command) => {
|
|||||||
menuCacheKeys.forEach(key => {
|
menuCacheKeys.forEach(key => {
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 遍历 sessionStorage
|
// 遍历 sessionStorage
|
||||||
const sessionMenuCacheKeys: string[] = [];
|
const sessionMenuCacheKeys: string[] = [];
|
||||||
for (let i = 0; i < sessionStorage.length; i++) {
|
for (let i = 0; i < sessionStorage.length; i++) {
|
||||||
@ -219,12 +210,12 @@ const handleCommand = async (command) => {
|
|||||||
sessionMenuCacheKeys.forEach(key => {
|
sessionMenuCacheKeys.forEach(key => {
|
||||||
sessionStorage.removeItem(key);
|
sessionStorage.removeItem(key);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 重置 tabs store 状态
|
// 重置 tabs store 状态
|
||||||
const { useTabsStore } = await import('@/stores');
|
const { useTabsStore } = await import('@/stores');
|
||||||
const tabsStore = useTabsStore();
|
const tabsStore = useTabsStore();
|
||||||
tabsStore.resetTabs();
|
tabsStore.resetTabs();
|
||||||
|
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -276,7 +267,7 @@ let handleChange: ((e: MediaQueryListEvent) => void) | null = null;
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initTheme();
|
initTheme();
|
||||||
|
|
||||||
// 监听系统主题变化
|
// 监听系统主题变化
|
||||||
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
handleChange = (e: MediaQueryListEvent) => {
|
handleChange = (e: MediaQueryListEvent) => {
|
||||||
@ -310,7 +301,7 @@ onUnmounted(() => {
|
|||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||||
|
|
||||||
// 亮色主题下使用 #062da3 背景
|
// 亮色主题下使用 #062da3 背景
|
||||||
html:not(.dark) & {
|
html:not(.dark) & {
|
||||||
background-color: #062da3;
|
background-color: #062da3;
|
||||||
@ -333,7 +324,7 @@ onUnmounted(() => {
|
|||||||
background-color: var(--el-fill-color-light);
|
background-color: var(--el-fill-color-light);
|
||||||
border-color: var(--el-border-color);
|
border-color: var(--el-border-color);
|
||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--el-fill-color);
|
background-color: var(--el-fill-color);
|
||||||
border-color: var(--el-border-color-dark);
|
border-color: var(--el-border-color-dark);
|
||||||
@ -348,7 +339,7 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
.refresh-cache-btn,
|
.refresh-cache-btn,
|
||||||
.theme-toggle-btn {
|
.theme-toggle-btn {
|
||||||
// 使用 Element Plus 的填充色变量
|
// 使用 Element Plus 的填充色变量
|
||||||
@ -356,14 +347,14 @@ onUnmounted(() => {
|
|||||||
border-color: var(--el-border-color);
|
border-color: var(--el-border-color);
|
||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--el-fill-color);
|
background-color: var(--el-fill-color);
|
||||||
border-color: var(--el-border-color-dark);
|
border-color: var(--el-border-color-dark);
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user {
|
.user {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@ -371,18 +362,18 @@ onUnmounted(() => {
|
|||||||
// 使用 Element Plus 的边框颜色变量
|
// 使用 Element Plus 的边框颜色变量
|
||||||
border: 2px solid var(--el-border-color);
|
border: 2px solid var(--el-border-color);
|
||||||
transition: border-color 0.3s ease;
|
transition: border-color 0.3s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--el-color-primary);
|
border-color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dropdown-link {
|
.el-dropdown-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|
||||||
.user-name {
|
.user-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
@ -391,7 +382,7 @@ onUnmounted(() => {
|
|||||||
max-width: 120px;
|
max-width: 120px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
// 亮色主题下使用白色
|
// 亮色主题下使用白色
|
||||||
html:not(.dark) & {
|
html:not(.dark) & {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
@ -407,27 +398,27 @@ onUnmounted(() => {
|
|||||||
background-color: var(--bg-color-overlay) !important;
|
background-color: var(--bg-color-overlay) !important;
|
||||||
border-color: var(--border-color) !important;
|
border-color: var(--border-color) !important;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
|
|
||||||
.el-dropdown-menu__item {
|
.el-dropdown-menu__item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
color: var(--text-color-primary) !important;
|
color: var(--text-color-primary) !important;
|
||||||
transition: background-color 0.2s ease, color 0.2s ease;
|
transition: background-color 0.2s ease, color 0.2s ease;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-icon {
|
.el-icon {
|
||||||
color: var(--text-color-primary) !important;
|
color: var(--text-color-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.is-disabled):hover {
|
&:not(.is-disabled):hover {
|
||||||
background-color: var(--fill-color-light) !important;
|
background-color: var(--fill-color-light) !important;
|
||||||
color: var(--text-color-primary) !important;
|
color: var(--text-color-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-divided {
|
&.is-divided {
|
||||||
border-top-color: var(--border-color) !important;
|
border-top-color: var(--border-color) !important;
|
||||||
}
|
}
|
||||||
@ -437,18 +428,23 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(.bread) {
|
:deep(.bread) {
|
||||||
|
|
||||||
// 面包屑使用白色
|
// 面包屑使用白色
|
||||||
.el-breadcrumb__inner {
|
.el-breadcrumb__inner {
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-breadcrumb__inner.is-link {
|
.el-breadcrumb__inner.is-link {
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
cursor: pointer !important;
|
cursor: pointer !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: rgba(255, 255, 255, 0.8) !important;
|
color: rgba(255, 255, 255, 0.8) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__nav){
|
||||||
|
height: 36px !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -9,6 +9,22 @@
|
|||||||
<el-descriptions-item label="进度">{{ Number(task.progress||0) }}%</el-descriptions-item>
|
<el-descriptions-item label="进度">{{ Number(task.progress||0) }}%</el-descriptions-item>
|
||||||
<el-descriptions-item label="截止时间" :span="2">{{ formatDateTime(task.plan_end_time) }}</el-descriptions-item>
|
<el-descriptions-item label="截止时间" :span="2">{{ formatDateTime(task.plan_end_time) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="描述" :span="2">{{ task.task_desc || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="描述" :span="2">{{ task.task_desc || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="附件" :span="2">
|
||||||
|
<div v-if="attachments.length" class="attachments">
|
||||||
|
<div v-for="file in attachments" :key="file.id" class="attachment-item">
|
||||||
|
<el-image
|
||||||
|
:src="file.url"
|
||||||
|
:preview-src-list="previewList"
|
||||||
|
fit="cover"
|
||||||
|
:hide-on-click-modal="true"
|
||||||
|
:initial-index="file.index"
|
||||||
|
style="width: 80px; height: 80px; border-radius: 6px; overflow: hidden;"
|
||||||
|
/>
|
||||||
|
<a :href="file.url" target="_blank" rel="noopener" class="attachment-link">{{ file.name }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="visible=false">关闭</el-button>
|
<el-button @click="visible=false">关闭</el-button>
|
||||||
@ -19,6 +35,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, computed } from 'vue'
|
import { ref, watch, computed } from 'vue'
|
||||||
import { useDictStore } from '@/stores/dict'
|
import { useDictStore } from '@/stores/dict'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
const props = defineProps<{ modelValue: boolean, task: any }>()
|
const props = defineProps<{ modelValue: boolean, task: any }>()
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
@ -39,7 +56,51 @@ const formatDateTime = (v: any) => {
|
|||||||
const pad = (n: number) => String(n).padStart(2, '0')
|
const pad = (n: number) => String(n).padStart(2, '0')
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const attachments = ref<Array<{ id: string|number, url: string, name: string, index: number }>>([])
|
||||||
|
const previewList = ref<string[]>([])
|
||||||
|
// 使用公共预览端点以避免 Authorization 头问题
|
||||||
|
const buildRelativePreview = (id: string|number) => `/api/files/public-preview/${id}`
|
||||||
|
const resolveFileUrl = (path: string): string => {
|
||||||
|
if (!path) return ''
|
||||||
|
if (/^https?:\/\//i.test(path)) return path
|
||||||
|
const base = (request as any)?.defaults?.baseURL || ''
|
||||||
|
try {
|
||||||
|
const origin = new URL(base, window.location.origin).origin
|
||||||
|
return origin.replace(/\/+$/, '') + '/' + String(path).replace(/^\/+/, '')
|
||||||
|
} catch {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.task, (t) => {
|
||||||
|
const ids: Array<string> = Array.isArray(t?.attachment_ids)
|
||||||
|
? (t?.attachment_ids as any[]).map((x: any) => String(x))
|
||||||
|
: (typeof t?.attachment_ids === 'string' && t.attachment_ids
|
||||||
|
? String(t.attachment_ids).split(',').map((x) => x.trim()).filter(Boolean)
|
||||||
|
: [])
|
||||||
|
previewList.value = ids.map(id => resolveFileUrl(buildRelativePreview(id)))
|
||||||
|
attachments.value = ids.map((id, idx) => ({ id, url: resolveFileUrl(buildRelativePreview(id)), name: id, index: idx }))
|
||||||
|
}, { immediate: true })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.attachments {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.attachment-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
.attachment-link {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -46,6 +46,25 @@
|
|||||||
<el-form-item label="描述" prop="task_desc">
|
<el-form-item label="描述" prop="task_desc">
|
||||||
<el-input v-model="form.task_desc" type="textarea" :rows="4" />
|
<el-input v-model="form.task_desc" type="textarea" :rows="4" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="附件">
|
||||||
|
<el-upload
|
||||||
|
:limit="5"
|
||||||
|
list-type="picture-card"
|
||||||
|
:file-list="uploadFileList"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:http-request="handleCustomUpload"
|
||||||
|
:on-success="handleUploadSuccess"
|
||||||
|
:on-remove="handleUploadRemove"
|
||||||
|
:on-preview="handleUploadPreview"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
</el-upload>
|
||||||
|
<el-dialog v-model="previewVisible" width="720px">
|
||||||
|
<img :src="previewImageUrl" style="width: 100%" />
|
||||||
|
</el-dialog>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<EmployeeSelectDialog
|
<EmployeeSelectDialog
|
||||||
v-model:visible="employeeDialogVisible"
|
v-model:visible="employeeDialogVisible"
|
||||||
@ -72,6 +91,9 @@ import { useDictStore } from '@/stores/dict'
|
|||||||
import EmployeeSelectDialog from './EmployeeSelectDialog.vue'
|
import EmployeeSelectDialog from './EmployeeSelectDialog.vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { getTenantEmployees } from '@/api/employee'
|
import { getTenantEmployees } from '@/api/employee'
|
||||||
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
|
import { uploadFile } from '@/api/file'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean,
|
modelValue: boolean,
|
||||||
@ -105,6 +127,22 @@ const form = reactive<any>({
|
|||||||
// 关联人(仅提交 id 数组在 team_employee_ids)
|
// 关联人(仅提交 id 数组在 team_employee_ids)
|
||||||
const teamEmployeeIds = ref<Array<number | string>>([])
|
const teamEmployeeIds = ref<Array<number | string>>([])
|
||||||
const teamEmployeeList = ref<Array<{id: number|string, name: string}>>([])
|
const teamEmployeeList = ref<Array<{id: number|string, name: string}>>([])
|
||||||
|
// 附件
|
||||||
|
const attachmentIds = ref<Array<number|string>>([])
|
||||||
|
const uploadFileList = ref<any[]>([])
|
||||||
|
const previewVisible = ref(false)
|
||||||
|
const previewImageUrl = ref('')
|
||||||
|
const resolveFileUrl = (path: string): string => {
|
||||||
|
if (!path) return ''
|
||||||
|
if (/^https?:\/\//i.test(path)) return path
|
||||||
|
const base = (request as any)?.defaults?.baseURL || ''
|
||||||
|
try {
|
||||||
|
const origin = new URL(base, window.location.origin).origin
|
||||||
|
return origin.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '')
|
||||||
|
} catch {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const loadTeamEmployeeNames = async (ids: Array<number | string>) => {
|
const loadTeamEmployeeNames = async (ids: Array<number | string>) => {
|
||||||
if (!ids || ids.length === 0) {
|
if (!ids || ids.length === 0) {
|
||||||
@ -183,6 +221,16 @@ watch(() => props.task, (t) => {
|
|||||||
} else {
|
} else {
|
||||||
loadTeamEmployeeNames(teamEmployeeIds.value)
|
loadTeamEmployeeNames(teamEmployeeIds.value)
|
||||||
}
|
}
|
||||||
|
// 初始化附件
|
||||||
|
if (Array.isArray(t?.attachment_ids)) {
|
||||||
|
attachmentIds.value = [...t.attachment_ids]
|
||||||
|
} else if (typeof t?.attachment_ids === 'string' && t.attachment_ids) {
|
||||||
|
attachmentIds.value = t.attachment_ids.split(',').map((x: string) => x.trim()).filter(Boolean)
|
||||||
|
} else {
|
||||||
|
attachmentIds.value = []
|
||||||
|
}
|
||||||
|
// 无法从任务上还原文件URL时,保持为空列表,由用户重新上传
|
||||||
|
uploadFileList.value = uploadFileList.value
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
const rules = reactive<FormRules<any>>({
|
const rules = reactive<FormRules<any>>({
|
||||||
@ -239,6 +287,14 @@ const onSubmit = async () => {
|
|||||||
const n = Number(x)
|
const n = Number(x)
|
||||||
return isNaN(n) ? x : n
|
return isNaN(n) ? x : n
|
||||||
})
|
})
|
||||||
|
// 追加附件 ID,逗号分隔(后端字段为 attachment_ids),如果返回了 id
|
||||||
|
if (attachmentIds.value.length) {
|
||||||
|
const ids = (attachmentIds.value || []).map((x: any) => {
|
||||||
|
const n = Number(x)
|
||||||
|
return isNaN(n) ? String(x) : String(n)
|
||||||
|
})
|
||||||
|
payload.attachment_ids = ids.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
if (props.isEdit && form.id) {
|
if (props.isEdit && form.id) {
|
||||||
res = await updateTask(form.id, payload)
|
res = await updateTask(form.id, payload)
|
||||||
@ -264,6 +320,63 @@ const onSubmit = async () => {
|
|||||||
const onClosed = () => {
|
const onClosed = () => {
|
||||||
// reset if needed
|
// reset if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 上传前校验(参考租户编辑)
|
||||||
|
const beforeUpload = (file: File) => {
|
||||||
|
const isImage = file.type.startsWith('image/')
|
||||||
|
const isLt5M = file.size / 1024 / 1024 < 5
|
||||||
|
if (!isImage) {
|
||||||
|
ElMessage.error('仅支持图片类型')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!isLt5M) {
|
||||||
|
ElMessage.error('图片大小不能超过 5MB')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义上传(参考租户编辑)
|
||||||
|
const handleCustomUpload = async (options: any) => {
|
||||||
|
const { file, onError, onSuccess } = options
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file as File)
|
||||||
|
const res = await uploadFile(formData, { category: '图片' })
|
||||||
|
onSuccess && onSuccess(res)
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error('上传失败')
|
||||||
|
onError && onError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传回调(兼容 id 与 file_url 返回)
|
||||||
|
const handleUploadSuccess = (response: any, file: any, fileList: any[]) => {
|
||||||
|
const id = response?.data?.id ?? response?.id
|
||||||
|
const rawUrl = response?.data?.file_url || response?.data?.url || response?.url || ''
|
||||||
|
if (id != null) {
|
||||||
|
attachmentIds.value.push(id)
|
||||||
|
}
|
||||||
|
const abs = rawUrl ? resolveFileUrl(rawUrl) : (file?.url || '')
|
||||||
|
// 用服务器返回的绝对地址覆盖当前项的 url,避免后续预览使用失效的 blob: URL
|
||||||
|
uploadFileList.value = fileList.map((f: any) => {
|
||||||
|
if (f.uid === file.uid) {
|
||||||
|
return { ...f, url: abs }
|
||||||
|
}
|
||||||
|
return { ...f, url: f.url }
|
||||||
|
})
|
||||||
|
if (abs) ElMessage.success('上传成功')
|
||||||
|
}
|
||||||
|
const handleUploadRemove = (file: any, fileList: any[]) => {
|
||||||
|
const id = file?.response?.data?.id ?? file?.name
|
||||||
|
attachmentIds.value = attachmentIds.value.filter(x => String(x) !== String(id))
|
||||||
|
uploadFileList.value = fileList
|
||||||
|
}
|
||||||
|
const handleUploadPreview = (file: any) => {
|
||||||
|
const url = file?.url || ''
|
||||||
|
previewImageUrl.value = url.startsWith('blob:') ? url : resolveFileUrl(url)
|
||||||
|
previewVisible.value = !!previewImageUrl.value
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link size="small" @click="openDetail(row)">查看</el-button>
|
<el-button link size="small" @click="openDetail(row)">查看</el-button>
|
||||||
<el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
|
<el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
|
||||||
<el-button link type="danger" size="small" @click="openDelete(row)">删除</el-button>
|
<el-button v-if="canDelete(row)" link type="danger" size="small" @click="openDelete(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -87,6 +87,7 @@ import { listTasks, getTask, deleteTask } from '@/api/tasks'
|
|||||||
import Edit from './components/edit.vue'
|
import Edit from './components/edit.vue'
|
||||||
import Detail from './components/detail.vue'
|
import Detail from './components/detail.vue'
|
||||||
import { useDictStore } from '@/stores/dict'
|
import { useDictStore } from '@/stores/dict'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
@ -110,6 +111,8 @@ const currentTask = ref<any>(null)
|
|||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false)
|
||||||
const deleteDialogVisible = ref(false) // 保留变量但不渲染独立组件
|
const deleteDialogVisible = ref(false) // 保留变量但不渲染独立组件
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const fetchList = async () => {
|
const fetchList = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@ -179,6 +182,13 @@ const openDelete = (row: any) => {
|
|||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 仅允许创建人删除
|
||||||
|
const canDelete = (row: any) => {
|
||||||
|
const uid = authStore?.user?.id
|
||||||
|
const cid = row?.creator_id ?? row?.creatorId
|
||||||
|
return uid != null && cid != null && String(uid) === String(cid)
|
||||||
|
}
|
||||||
|
|
||||||
const priorityTagType = (v: string) => {
|
const priorityTagType = (v: string) => {
|
||||||
if (v === 'urgent') return 'danger'
|
if (v === 'urgent') return 'danger'
|
||||||
if (v === 'high') return 'warning'
|
if (v === 'high') return 'warning'
|
||||||
|
|||||||
@ -356,6 +356,7 @@ import { useRouter } from "vue-router";
|
|||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { getKnowledgeList } from "@/api/knowledge";
|
import { getKnowledgeList } from "@/api/knowledge";
|
||||||
|
import { getTodoTasks } from "@/api/tasks";
|
||||||
import {
|
import {
|
||||||
FolderOpened,
|
FolderOpened,
|
||||||
StarFilled,
|
StarFilled,
|
||||||
@ -605,78 +606,24 @@ const industryNews = ref([
|
|||||||
const sortType = ref("time");
|
const sortType = ref("time");
|
||||||
|
|
||||||
// 待办事项
|
// 待办事项
|
||||||
const tasks = ref([
|
const tasks = ref([]);
|
||||||
{
|
|
||||||
id: 1,
|
async function fetchTodoTasks() {
|
||||||
title: "设计部李亦想的加班申请流程",
|
try {
|
||||||
node: "直属领导审批",
|
const tenantId = authStore?.user?.tenant_id || 0;
|
||||||
applyTime: "2024-01-20 13:32",
|
const res = await getTodoTasks({ tenantId, limit: 10 });
|
||||||
system: "oa",
|
const list = res?.data?.list || res?.data || [];
|
||||||
},
|
tasks.value = list.map((item) => ({
|
||||||
{
|
id: item.id,
|
||||||
id: 2,
|
title: item.task_name || item.title || "",
|
||||||
title: "财务部张申请的报销流程",
|
node: item.node || item.task_status || "",
|
||||||
node: "财务审核",
|
applyTime: item.apply_time || item.created_time || item.createdAt || "",
|
||||||
applyTime: "2024-01-19 10:15",
|
system: item.system || "oa",
|
||||||
system: "finance",
|
}));
|
||||||
},
|
} catch (e) {
|
||||||
{
|
console.error("获取待办任务失败", e);
|
||||||
id: 3,
|
}
|
||||||
title: "人事部王申请的请假流程",
|
}
|
||||||
node: "人事审批",
|
|
||||||
applyTime: "2024-01-18 14:20",
|
|
||||||
system: "oa",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: "技术部赵申请的出差申请",
|
|
||||||
node: "部门经理审批",
|
|
||||||
applyTime: "2024-01-17 09:30",
|
|
||||||
system: "oa",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: "市场部钱申请的采购申请",
|
|
||||||
node: "采购部门审批",
|
|
||||||
applyTime: "2024-01-16 15:45",
|
|
||||||
system: "oa",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
title: "销售部孙申请的合同审批",
|
|
||||||
node: "法务部门审批",
|
|
||||||
applyTime: "2024-01-15 11:20",
|
|
||||||
system: "oa",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
title: "运营部周申请的预算申请",
|
|
||||||
node: "财务部门审批",
|
|
||||||
applyTime: "2024-01-14 16:10",
|
|
||||||
system: "finance",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
title: "产品部吴申请的项目立项",
|
|
||||||
node: "总经理审批",
|
|
||||||
applyTime: "2024-01-13 08:50",
|
|
||||||
system: "oa",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
title: "研发部郑申请的设备采购",
|
|
||||||
node: "采购部门审批",
|
|
||||||
applyTime: "2024-01-12 14:25",
|
|
||||||
system: "oa",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
title: "行政部王申请的办公用品采购",
|
|
||||||
node: "行政主管审批",
|
|
||||||
applyTime: "2024-01-11 10:00",
|
|
||||||
system: "oa",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const filteredTasks = computed(() => {
|
const filteredTasks = computed(() => {
|
||||||
return tasks.value;
|
return tasks.value;
|
||||||
@ -885,6 +832,8 @@ onMounted(() => {
|
|||||||
document.addEventListener("click", handleClickOutside);
|
document.addEventListener("click", handleClickOutside);
|
||||||
// 加载知识库数据
|
// 加载知识库数据
|
||||||
fetchKnowledgeList();
|
fetchKnowledgeList();
|
||||||
|
// 加载我的待办
|
||||||
|
fetchTodoTasks();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@ -93,16 +93,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="list-content">
|
<div class="list-content">
|
||||||
<div v-for="(task, idx) in paginatedTasks" :key="idx" class="task-item" :class="{ done: task.completed }">
|
<div v-for="(task, idx) in paginatedTasks" :key="idx" class="task-item" :class="{ done: task.completed }">
|
||||||
<el-checkbox v-model="task.completed" @change="handleTaskChange(task)" />
|
<!-- <el-checkbox v-model="task.completed" @change="handleTaskChange(task)" /> -->
|
||||||
<div class="task-info">
|
<div class="task-info">
|
||||||
<div class="task-title">
|
<div class="task-title">
|
||||||
{{ task.title }}
|
{{ task.title }}
|
||||||
<el-tag :type="getPriorityType(task.priority)" size="small" effect="plain">
|
<el-tag :type="task.priorityTagType || getPriorityType(task.priority)" size="small" effect="plain" :class="'priority-' + task.priority">
|
||||||
{{ task.priority }}
|
{{ task.priorityLabel || task.priority }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-meta">
|
<div class="task-meta">
|
||||||
<span class="task-date">{{ task.date }}</span>
|
<span class="task-date">
|
||||||
|
<el-icon><Clock /></el-icon>
|
||||||
|
{{ formatDate(task.date) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -164,8 +167,10 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from "@element-plus/icons-vue";
|
} from "@element-plus/icons-vue";
|
||||||
import { getKnowledgeCount } from "@/api/knowledge";
|
import { getKnowledgeCount } from "@/api/knowledge";
|
||||||
|
import { getTodoTasks } from "@/api/tasks";
|
||||||
import { getPlatformStats, getTenantStats, getActivityLogs } from "@/api/dashboard";
|
import { getPlatformStats, getTenantStats, getActivityLogs } from "@/api/dashboard";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { useDictStore } from '@/stores/dict'
|
||||||
|
|
||||||
Chart.register(...registerables);
|
Chart.register(...registerables);
|
||||||
|
|
||||||
@ -179,6 +184,14 @@ const currentDate = computed(() => {
|
|||||||
return `${month}月${day}日 ${weekday}`;
|
return `${month}月${day}日 ${weekday}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//格式化时间
|
||||||
|
const formatDate = (date: string) => {
|
||||||
|
const d = new Date(date);
|
||||||
|
const month = d.getMonth() + 1;
|
||||||
|
const day = d.getDate();
|
||||||
|
return `${month}月${day}日`;
|
||||||
|
};
|
||||||
|
|
||||||
// 统计数据(使用 markRaw 避免图标组件被响应式化)
|
// 统计数据(使用 markRaw 避免图标组件被响应式化)
|
||||||
const stats = ref([
|
const stats = ref([
|
||||||
{
|
{
|
||||||
@ -288,56 +301,48 @@ const fetchTenantStats = async () => {
|
|||||||
const taskCurrentPage = ref(1);
|
const taskCurrentPage = ref(1);
|
||||||
const taskPageSize = ref(5);
|
const taskPageSize = ref(5);
|
||||||
|
|
||||||
const tasks = ref([
|
const tasks = ref<any[]>([]);
|
||||||
{
|
const priorityDict = ref<any[]>([]);
|
||||||
title: "完成Q2预算审核",
|
|
||||||
date: "2024-06-11",
|
async function fetchPriorityDict() {
|
||||||
priority: "High",
|
try {
|
||||||
completed: false,
|
const dictStore = useDictStore();
|
||||||
},
|
priorityDict.value = await dictStore.getDictItems('task_priority');
|
||||||
{
|
} catch (e) {
|
||||||
title: "发邮件通知团队",
|
// ignore
|
||||||
date: "2024-06-10",
|
}
|
||||||
priority: "Medium",
|
}
|
||||||
completed: true,
|
|
||||||
},
|
async function fetchTodoTasksDashboard() {
|
||||||
{
|
try {
|
||||||
title: "产品需求讨论会",
|
const tenantId = authStore?.user?.tenant_id || 0;
|
||||||
date: "2024-06-09",
|
const id = authStore?.user?.id || 0;
|
||||||
priority: "Low",
|
const res = await getTodoTasks({ tenantId, id, limit: 20 });
|
||||||
completed: false,
|
const list = res?.data?.list || res?.data || [];
|
||||||
},
|
// 映射到仪表盘展示所需结构
|
||||||
{
|
tasks.value = list.map((item: any) => ({
|
||||||
title: "准备季度报告",
|
title: item.task_name || item.title || "",
|
||||||
date: "2024-06-12",
|
date: item.created_time || item.apply_time || item.updated_time || "",
|
||||||
priority: "High",
|
priority: item.priority || "",
|
||||||
completed: false,
|
priorityLabel: (() => {
|
||||||
},
|
const dictItem = priorityDict.value.find((d: any) => String(d.dict_value) === String(item.priority));
|
||||||
{
|
return dictItem?.dict_label || (item.priority ? capitalize(item.priority) : 'Medium');
|
||||||
title: "更新项目文档",
|
})(),
|
||||||
date: "2024-06-13",
|
priorityTagType: (() => {
|
||||||
priority: "Medium",
|
const dictItem = priorityDict.value.find((d: any) => String(d.dict_value) === String(item.priority));
|
||||||
completed: false,
|
return dictItem?.dict_tag_type || undefined;
|
||||||
},
|
})(),
|
||||||
{
|
completed: item.task_status === 'completed' || item.task_status === 'closed',
|
||||||
title: "代码审查",
|
}));
|
||||||
date: "2024-06-14",
|
} catch (e) {
|
||||||
priority: "Low",
|
console.warn('获取待办任务失败', e);
|
||||||
completed: false,
|
}
|
||||||
},
|
}
|
||||||
{
|
|
||||||
title: "客户需求沟通",
|
function capitalize(s: string) {
|
||||||
date: "2024-06-15",
|
if (!s) return s;
|
||||||
priority: "High",
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||||
completed: false,
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "测试环境部署",
|
|
||||||
date: "2024-06-16",
|
|
||||||
priority: "Medium",
|
|
||||||
completed: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 分页后的任务列表
|
// 分页后的任务列表
|
||||||
const paginatedTasks = computed(() => {
|
const paginatedTasks = computed(() => {
|
||||||
@ -445,130 +450,128 @@ onMounted(() => {
|
|||||||
// 加载活动日志
|
// 加载活动日志
|
||||||
fetchActivityLogs();
|
fetchActivityLogs();
|
||||||
|
|
||||||
|
// 加载待办任务
|
||||||
|
fetchPriorityDict();
|
||||||
|
fetchTodoTasksDashboard();
|
||||||
|
|
||||||
// 折线图
|
// 折线图
|
||||||
const lineChartEl = document.getElementById("lineChart") as HTMLCanvasElement | null;
|
const lineChartEl = document.getElementById("lineChart") as HTMLCanvasElement | null;
|
||||||
if (!lineChartEl) {
|
if (lineChartEl) {
|
||||||
console.error("Line chart element not found");
|
new Chart(lineChartEl, {
|
||||||
return;
|
type: "line",
|
||||||
|
data: {
|
||||||
|
labels: ["1月", "2月", "3月", "4月", "5月", "6月", "7月"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "收入(元)",
|
||||||
|
data: [16800, 19400, 23100, 24600, 27600, 31000, 35800],
|
||||||
|
fill: true,
|
||||||
|
borderColor: "#4f84ff",
|
||||||
|
backgroundColor: "rgba(79, 132, 255, 0.1)",
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 4,
|
||||||
|
pointHoverRadius: 6,
|
||||||
|
pointBackgroundColor: "#4f84ff",
|
||||||
|
pointBorderColor: "#fff",
|
||||||
|
pointBorderWidth: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||||
|
padding: 12,
|
||||||
|
titleFont: { size: 14 },
|
||||||
|
bodyFont: { size: 13 },
|
||||||
|
cornerRadius: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
color: "var(--el-text-color-regular)",
|
||||||
|
font: { size: 12 },
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: "var(--el-border-color-lighter)",
|
||||||
|
drawBorder: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
color: "var(--el-text-color-regular)",
|
||||||
|
font: { size: 12 },
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
new Chart(lineChartEl, {
|
|
||||||
type: "line",
|
|
||||||
data: {
|
|
||||||
labels: ["1月", "2月", "3月", "4月", "5月", "6月", "7月"],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "收入(元)",
|
|
||||||
data: [16800, 19400, 23100, 24600, 27600, 31000, 35800],
|
|
||||||
fill: true,
|
|
||||||
borderColor: "#4f84ff",
|
|
||||||
backgroundColor: "rgba(79, 132, 255, 0.1)",
|
|
||||||
tension: 0.4,
|
|
||||||
pointRadius: 4,
|
|
||||||
pointHoverRadius: 6,
|
|
||||||
pointBackgroundColor: "#4f84ff",
|
|
||||||
pointBorderColor: "#fff",
|
|
||||||
pointBorderWidth: 2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: { display: false },
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
|
||||||
padding: 12,
|
|
||||||
titleFont: { size: 14 },
|
|
||||||
bodyFont: { size: 13 },
|
|
||||||
cornerRadius: 6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
beginAtZero: true,
|
|
||||||
ticks: {
|
|
||||||
color: "var(--el-text-color-regular)",
|
|
||||||
font: { size: 12 },
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
color: "var(--el-border-color-lighter)",
|
|
||||||
drawBorder: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
ticks: {
|
|
||||||
color: "var(--el-text-color-regular)",
|
|
||||||
font: { size: 12 },
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 柱状图
|
// 柱状图
|
||||||
const barChartEl = document.getElementById("barChart") as HTMLCanvasElement | null;
|
const barChartEl = document.getElementById("barChart") as HTMLCanvasElement | null;
|
||||||
if (!barChartEl) {
|
if (barChartEl) {
|
||||||
console.error("Bar chart element not found");
|
new Chart(barChartEl, {
|
||||||
return;
|
type: "bar",
|
||||||
|
data: {
|
||||||
|
labels: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "活跃用户",
|
||||||
|
data: [140, 162, 189, 176, 206, 238, 205],
|
||||||
|
backgroundColor: "rgba(79, 132, 255, 0.8)",
|
||||||
|
borderRadius: 6,
|
||||||
|
barPercentage: 0.6,
|
||||||
|
borderSkipped: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||||
|
padding: 12,
|
||||||
|
titleFont: { size: 14 },
|
||||||
|
bodyFont: { size: 13 },
|
||||||
|
cornerRadius: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
color: "var(--el-text-color-regular)",
|
||||||
|
font: { size: 12 },
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: "var(--el-border-color-lighter)",
|
||||||
|
drawBorder: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
color: "var(--el-text-color-regular)",
|
||||||
|
font: { size: 12 },
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
new Chart(barChartEl, {
|
|
||||||
type: "bar",
|
|
||||||
data: {
|
|
||||||
labels: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "活跃用户",
|
|
||||||
data: [140, 162, 189, 176, 206, 238, 205],
|
|
||||||
backgroundColor: "rgba(79, 132, 255, 0.8)",
|
|
||||||
borderRadius: 6,
|
|
||||||
barPercentage: 0.6,
|
|
||||||
borderSkipped: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: { display: false },
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
|
||||||
padding: 12,
|
|
||||||
titleFont: { size: 14 },
|
|
||||||
bodyFont: { size: 13 },
|
|
||||||
cornerRadius: 6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
beginAtZero: true,
|
|
||||||
ticks: {
|
|
||||||
color: "var(--el-text-color-regular)",
|
|
||||||
font: { size: 12 },
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
color: "var(--el-border-color-lighter)",
|
|
||||||
drawBorder: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
ticks: {
|
|
||||||
color: "var(--el-text-color-regular)",
|
|
||||||
font: { size: 12 },
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -822,6 +825,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
&.done {
|
&.done {
|
||||||
.task-info {
|
.task-info {
|
||||||
|
margin-left: 20px;
|
||||||
.task-title {
|
.task-title {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
color: var(--el-text-color-placeholder);
|
color: var(--el-text-color-placeholder);
|
||||||
@ -978,4 +982,22 @@ onMounted(() => {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.priority-0 {
|
||||||
|
color: #e6a23c;
|
||||||
|
background-color: #fdf6ec;
|
||||||
|
border-color: #f5dab1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priority-1 {
|
||||||
|
color: #67c23a;
|
||||||
|
background-color: rgb(240, 249, 235);
|
||||||
|
border-color: rgb(225, 243, 216);
|
||||||
|
}
|
||||||
|
|
||||||
|
.priority-2 {
|
||||||
|
color: #f56c6c;
|
||||||
|
background-color: #fef0f0;
|
||||||
|
border-color: #fbc4c4;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,11 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog title="租户详情" v-model="visible" width="600px" :close-on-click-modal="false" @close="handleClose">
|
||||||
title="租户详情"
|
|
||||||
v-model="visible"
|
|
||||||
width="600px"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
@close="handleClose"
|
|
||||||
>
|
|
||||||
<div class="tenant-detail" v-loading="loading">
|
<div class="tenant-detail" v-loading="loading">
|
||||||
<el-descriptions :column="2" border>
|
<el-descriptions :column="2" border>
|
||||||
<el-descriptions-item label="租户ID">
|
<el-descriptions-item label="租户ID">
|
||||||
@ -14,9 +8,6 @@
|
|||||||
<el-descriptions-item label="租户名称">
|
<el-descriptions-item label="租户名称">
|
||||||
{{ tenantData.name }}
|
{{ tenantData.name }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="租户编码">
|
|
||||||
{{ tenantData.code }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="负责人">
|
<el-descriptions-item label="负责人">
|
||||||
{{ tenantData.owner }}
|
{{ tenantData.owner }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@ -27,12 +18,12 @@
|
|||||||
{{ tenantData.email || '未设置' }}
|
{{ tenantData.email || '未设置' }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="审核状态">
|
<el-descriptions-item label="审核状态">
|
||||||
<el-tag :type="getAuditStatusType(tenantData.audit_status)">
|
<el-tag :type="getAuditStatusType(tenantData.audit_status)" :class="`${tenantData.audit_status}-color`">
|
||||||
{{ getAuditStatusText(tenantData.audit_status) }}
|
{{ getAuditStatusText(tenantData.audit_status) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="状态">
|
<el-descriptions-item label="状态">
|
||||||
<el-tag :type="getTenantStatusType(tenantData.status)">
|
<el-tag :type="getTenantStatusType(tenantData.status)" :class="`color-${tenantData.status}`">
|
||||||
{{ getTenantStatusText(tenantData.status) }}
|
{{ getTenantStatusText(tenantData.status) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@ -40,19 +31,28 @@
|
|||||||
<span>{{ formatCapacity(tenantData.capacity) }}</span>
|
<span>{{ formatCapacity(tenantData.capacity) }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="已使用">
|
<el-descriptions-item label="已使用">
|
||||||
<span>{{ formatCapacity(tenantData.capacity_used || 0) }}</span>
|
<div style="display: flex;flex-direction: column;">
|
||||||
<el-progress
|
<span>{{ formatCapacity(tenantData.capacity_used || 0) }}</span>
|
||||||
:percentage="getCapacityPercentage(tenantData.capacity, tenantData.capacity_used)"
|
<el-progress :percentage="getCapacityPercentage(tenantData.capacity, tenantData.capacity_used)"
|
||||||
:color="getCapacityColor(tenantData.capacity, tenantData.capacity_used)"
|
:color="getCapacityColor(tenantData.capacity, tenantData.capacity_used)" :stroke-width="6"
|
||||||
:stroke-width="6"
|
style="margin-top: 8px;" />
|
||||||
style="margin-top: 8px; width: 200px;"
|
</div>
|
||||||
/>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="附件" :span="2">
|
||||||
|
<template v-if="tenantData.attachment_url">
|
||||||
|
<el-image :src="resolveFileUrl(tenantData.attachment_url)"
|
||||||
|
:preview-src-list="[resolveFileUrl(tenantData.attachment_url)]" fit="cover"
|
||||||
|
style="width: 160px; height: 120px; border-radius: 4px;" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
无
|
||||||
|
</template>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">
|
<el-descriptions-item label="创建时间">
|
||||||
{{ tenantData.created_at }}
|
{{ formatTime(tenantData.created_at) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="更新时间">
|
<el-descriptions-item label="更新时间">
|
||||||
{{ tenantData.updated_at || '未更新' }}
|
{{ formatTime(tenantData.updated_at) || '未更新' }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="备注" :span="2">
|
<el-descriptions-item label="备注" :span="2">
|
||||||
{{ tenantData.remark || '无' }}
|
{{ tenantData.remark || '无' }}
|
||||||
@ -69,7 +69,9 @@
|
|||||||
import { ref, watch, computed, onMounted } from 'vue';
|
import { ref, watch, computed, onMounted } from 'vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { getTenantDetail } from '@/api/tenant';
|
import { getTenantDetail } from '@/api/tenant';
|
||||||
|
import request from '@/utils/request';
|
||||||
import { useDictStore } from '@/stores/dict';
|
import { useDictStore } from '@/stores/dict';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean;
|
modelValue: boolean;
|
||||||
@ -125,7 +127,12 @@ function getAuditStatusType(status: string): 'warning' | 'success' | 'danger' |
|
|||||||
if (dictItem) {
|
if (dictItem) {
|
||||||
return dictItem.dict_tag_type || 'info';
|
return dictItem.dict_tag_type || 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//格式化时间
|
||||||
|
function formatTime(time: string | number | Date): string {
|
||||||
|
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
}
|
||||||
|
|
||||||
// 兜底方案,保持原有逻辑
|
// 兜底方案,保持原有逻辑
|
||||||
const statusMap: Record<string, 'warning' | 'success' | 'danger' | 'info'> = {
|
const statusMap: Record<string, 'warning' | 'success' | 'danger' | 'info'> = {
|
||||||
pending: 'warning',
|
pending: 'warning',
|
||||||
@ -143,7 +150,7 @@ function getAuditStatusText(status: string) {
|
|||||||
if (dictItem) {
|
if (dictItem) {
|
||||||
return dictItem.dict_label || String(status);
|
return dictItem.dict_label || String(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底方案,保持原有逻辑
|
// 兜底方案,保持原有逻辑
|
||||||
const statusMap: Record<string, string> = {
|
const statusMap: Record<string, string> = {
|
||||||
pending: '待审核',
|
pending: '待审核',
|
||||||
@ -162,7 +169,7 @@ function getTenantStatusType(status: string | number): 'success' | 'info' | 'war
|
|||||||
if (dictItem) {
|
if (dictItem) {
|
||||||
return dictItem.dict_tag_type || 'info';
|
return dictItem.dict_tag_type || 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底方案,保持原有逻辑
|
// 兜底方案,保持原有逻辑
|
||||||
return String(status) === '1' ? 'success' : 'info';
|
return String(status) === '1' ? 'success' : 'info';
|
||||||
}
|
}
|
||||||
@ -175,7 +182,7 @@ function getTenantStatusText(status: string | number): string {
|
|||||||
if (dictItem) {
|
if (dictItem) {
|
||||||
return dictItem.dict_label || String(status);
|
return dictItem.dict_label || String(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底方案,保持原有逻辑
|
// 兜底方案,保持原有逻辑
|
||||||
return String(status) === '1' ? '启用' : '禁用';
|
return String(status) === '1' ? '启用' : '禁用';
|
||||||
}
|
}
|
||||||
@ -213,10 +220,22 @@ function getCapacityColor(capacity: number | undefined, used: number | undefined
|
|||||||
return '#67c23a'; // 绿色
|
return '#67c23a'; // 绿色
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveFileUrl(path: string): string {
|
||||||
|
if (!path) return '';
|
||||||
|
if (/^https?:\/\//i.test(path)) return path;
|
||||||
|
const base = (request as any)?.defaults?.baseURL || '';
|
||||||
|
if (!base) return path;
|
||||||
|
return base.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(time: string | number | Date): string {
|
||||||
|
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
}
|
||||||
|
|
||||||
// 获取租户详情
|
// 获取租户详情
|
||||||
async function fetchDetail() {
|
async function fetchDetail() {
|
||||||
if (!props.tenantId) return;
|
if (!props.tenantId) return;
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await getTenantDetail(props.tenantId);
|
const res = await getTenantDetail(props.tenantId);
|
||||||
@ -277,6 +296,11 @@ watch(
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__cell) {
|
||||||
|
width: 180px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-descriptions-item__label) {
|
:deep(.el-descriptions-item__label) {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--el-text-color-regular);
|
color: var(--el-text-color-regular);
|
||||||
@ -286,5 +310,23 @@ watch(
|
|||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
.pending-color {
|
||||||
|
color: #e6a23c;
|
||||||
|
background-color: #fdf6ec;
|
||||||
|
border-color: #f5dab1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approved-color,
|
||||||
|
.color-1 {
|
||||||
|
color: #67c23a;
|
||||||
|
background-color: rgb(240, 249, 235);
|
||||||
|
border-color: rgb(225, 243, 216);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rejected-color {
|
||||||
|
color: #f56c6c;
|
||||||
|
background-color: #fef0f0;
|
||||||
|
border-color: #fbc4c4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -15,9 +15,6 @@
|
|||||||
<el-form-item label="租户名称" prop="name">
|
<el-form-item label="租户名称" prop="name">
|
||||||
<el-input v-model="tenantForm.name" placeholder="请输入租户名称" />
|
<el-input v-model="tenantForm.name" placeholder="请输入租户名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="租户编码" prop="code">
|
|
||||||
<el-input v-model="tenantForm.code" placeholder="请输入租户编码" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="负责人" prop="owner">
|
<el-form-item label="负责人" prop="owner">
|
||||||
<el-input v-model="tenantForm.owner" placeholder="请输入负责人" />
|
<el-input v-model="tenantForm.owner" placeholder="请输入负责人" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -56,6 +53,24 @@
|
|||||||
placeholder="请输入备注"
|
placeholder="请输入备注"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="附件">
|
||||||
|
<el-upload
|
||||||
|
:limit="1"
|
||||||
|
list-type="picture-card"
|
||||||
|
:file-list="uploadFileList"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:http-request="handleCustomUpload"
|
||||||
|
:on-success="handleUploadSuccess"
|
||||||
|
:on-remove="handleRemove"
|
||||||
|
:on-preview="handlePreview"
|
||||||
|
accept="image/*"
|
||||||
|
>
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
</el-upload>
|
||||||
|
<el-dialog v-model="previewVisible" width="500px">
|
||||||
|
<img :src="previewUrl" style="width: 100%" />
|
||||||
|
</el-dialog>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="handleClose">取消</el-button>
|
<el-button @click="handleClose">取消</el-button>
|
||||||
@ -73,8 +88,14 @@ import {
|
|||||||
ElMessageBox,
|
ElMessageBox,
|
||||||
type FormInstance,
|
type FormInstance,
|
||||||
type FormRules,
|
type FormRules,
|
||||||
|
type UploadFile,
|
||||||
|
type UploadUserFile,
|
||||||
|
type UploadRequestOptions,
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
import { Plus } from '@element-plus/icons-vue';
|
||||||
import { createTenant, updateTenant } from '@/api/tenant';
|
import { createTenant, updateTenant } from '@/api/tenant';
|
||||||
|
import { uploadFile } from '@/api/file';
|
||||||
|
import request from '@/utils/request';
|
||||||
import { useDictStore } from '@/stores/dict';
|
import { useDictStore } from '@/stores/dict';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -99,6 +120,9 @@ const visible = computed({
|
|||||||
|
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const tenantFormRef = ref<FormInstance>();
|
const tenantFormRef = ref<FormInstance>();
|
||||||
|
const uploadFileList = ref<UploadUserFile[]>([]);
|
||||||
|
const previewVisible = ref(false);
|
||||||
|
const previewUrl = ref('');
|
||||||
|
|
||||||
// 字典数据
|
// 字典数据
|
||||||
const dictStore = useDictStore();
|
const dictStore = useDictStore();
|
||||||
@ -122,18 +146,17 @@ const isEditing = computed(() => {
|
|||||||
const tenantForm = reactive({
|
const tenantForm = reactive({
|
||||||
id: null as number | null,
|
id: null as number | null,
|
||||||
name: '',
|
name: '',
|
||||||
code: '',
|
|
||||||
owner: '',
|
owner: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
email: '',
|
email: '',
|
||||||
status: '1', // 改为字符串类型,与字典数据中的dict_value保持一致
|
status: '1', // 改为字符串类型,与字典数据中的dict_value保持一致
|
||||||
capacity: 0,
|
capacity: 0,
|
||||||
remark: '',
|
remark: '',
|
||||||
|
attachment_url: '' as string,
|
||||||
});
|
});
|
||||||
|
|
||||||
const formRules: FormRules = {
|
const formRules: FormRules = {
|
||||||
name: [{ required: true, message: '请输入租户名称', trigger: 'blur' }],
|
name: [{ required: true, message: '请输入租户名称', trigger: 'blur' }],
|
||||||
code: [{ required: true, message: '请输入租户编码', trigger: 'blur' }],
|
|
||||||
owner: [{ required: true, message: '请输入负责人', trigger: 'blur' }],
|
owner: [{ required: true, message: '请输入负责人', trigger: 'blur' }],
|
||||||
phone: [
|
phone: [
|
||||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' },
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' },
|
||||||
@ -146,17 +169,30 @@ const formRules: FormRules = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resolveFileUrl(path: string): string {
|
||||||
|
if (!path) return '';
|
||||||
|
if (/^https?:\/\//i.test(path)) return path;
|
||||||
|
const base = (request as any)?.defaults?.baseURL || '';
|
||||||
|
try {
|
||||||
|
const origin = new URL(base, window.location.origin).origin;
|
||||||
|
return origin.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
|
||||||
|
} catch {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 重置表单
|
// 重置表单
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
tenantForm.id = null;
|
tenantForm.id = null;
|
||||||
tenantForm.name = '';
|
tenantForm.name = '';
|
||||||
tenantForm.code = '';
|
|
||||||
tenantForm.owner = '';
|
tenantForm.owner = '';
|
||||||
tenantForm.phone = '';
|
tenantForm.phone = '';
|
||||||
tenantForm.email = '';
|
tenantForm.email = '';
|
||||||
tenantForm.status = '1'; // 改为字符串类型,与字典数据中的dict_value保持一致
|
tenantForm.status = '1'; // 改为字符串类型,与字典数据中的dict_value保持一致
|
||||||
tenantForm.capacity = 0;
|
tenantForm.capacity = 0;
|
||||||
tenantForm.remark = '';
|
tenantForm.remark = '';
|
||||||
|
tenantForm.attachment_url = '';
|
||||||
|
uploadFileList.value = [];
|
||||||
tenantFormRef.value?.resetFields();
|
tenantFormRef.value?.resetFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,7 +201,6 @@ function initFormData() {
|
|||||||
if (props.tenant && props.tenant.id) {
|
if (props.tenant && props.tenant.id) {
|
||||||
tenantForm.id = props.tenant.id;
|
tenantForm.id = props.tenant.id;
|
||||||
tenantForm.name = props.tenant.name || '';
|
tenantForm.name = props.tenant.name || '';
|
||||||
tenantForm.code = props.tenant.code || '';
|
|
||||||
tenantForm.owner = props.tenant.owner || '';
|
tenantForm.owner = props.tenant.owner || '';
|
||||||
tenantForm.phone = props.tenant.phone || '';
|
tenantForm.phone = props.tenant.phone || '';
|
||||||
tenantForm.email = props.tenant.email || '';
|
tenantForm.email = props.tenant.email || '';
|
||||||
@ -174,6 +209,10 @@ function initFormData() {
|
|||||||
// capacity 后端返回的是 MB,直接使用
|
// capacity 后端返回的是 MB,直接使用
|
||||||
tenantForm.capacity = props.tenant.capacity != null ? Number(props.tenant.capacity) : 0;
|
tenantForm.capacity = props.tenant.capacity != null ? Number(props.tenant.capacity) : 0;
|
||||||
tenantForm.remark = props.tenant.remark || '';
|
tenantForm.remark = props.tenant.remark || '';
|
||||||
|
tenantForm.attachment_url = props.tenant.attachment_url || '';
|
||||||
|
uploadFileList.value = tenantForm.attachment_url
|
||||||
|
? [{ name: '附件', url: resolveFileUrl(tenantForm.attachment_url) }]
|
||||||
|
: [];
|
||||||
} else {
|
} else {
|
||||||
resetForm();
|
resetForm();
|
||||||
}
|
}
|
||||||
@ -188,14 +227,14 @@ async function handleSubmit() {
|
|||||||
try {
|
try {
|
||||||
const tenantData = {
|
const tenantData = {
|
||||||
name: tenantForm.name,
|
name: tenantForm.name,
|
||||||
code: tenantForm.code,
|
|
||||||
owner: tenantForm.owner,
|
owner: tenantForm.owner,
|
||||||
phone: tenantForm.phone || '',
|
phone: tenantForm.phone || '',
|
||||||
email: tenantForm.email || '',
|
email: tenantForm.email || '',
|
||||||
status: Number(tenantForm.status), // 提交时转换为数字类型
|
status: Number(tenantForm.status), // 提交时转换为数字类型
|
||||||
// capacity 前后端都使用 MB
|
// capacity 前后端都使用 MB
|
||||||
capacity: tenantForm.capacity || 0,
|
capacity: tenantForm.capacity || 0,
|
||||||
remark: tenantForm.remark || '',
|
remark: tenantForm.remark || '',
|
||||||
|
attachment_url: tenantForm.attachment_url || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
let res;
|
let res;
|
||||||
@ -253,6 +292,55 @@ watch(
|
|||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const beforeUpload = (file: File) => {
|
||||||
|
const isImage = file.type.startsWith('image/');
|
||||||
|
const isLt5M = file.size / 1024 / 1024 < 5;
|
||||||
|
if (!isImage) {
|
||||||
|
ElMessage.error('仅支持图片类型');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isLt5M) {
|
||||||
|
ElMessage.error('图片大小不能超过 5MB');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUploadSuccess = (response: any, file: UploadFile) => {
|
||||||
|
const raw = response?.data?.file_url || response?.data?.url || response?.url || '';
|
||||||
|
if (raw) {
|
||||||
|
tenantForm.attachment_url = raw;
|
||||||
|
const abs = resolveFileUrl(raw);
|
||||||
|
uploadFileList.value = [{ name: file.name, url: abs }];
|
||||||
|
ElMessage.success('上传成功');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('上传成功但未返回URL');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = () => {
|
||||||
|
tenantForm.attachment_url = '';
|
||||||
|
uploadFileList.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreview = (file: UploadFile) => {
|
||||||
|
previewUrl.value = (file.url as string) || tenantForm.attachment_url || '';
|
||||||
|
if (previewUrl.value) previewVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomUpload = async (options: UploadRequestOptions) => {
|
||||||
|
const { file, onError, onSuccess } = options as any;
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file as File);
|
||||||
|
const res = await uploadFile(formData, { category: '图片' });
|
||||||
|
onSuccess && onSuccess(res);
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error('上传失败');
|
||||||
|
onError && onError(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
@ -262,4 +350,3 @@ watch(
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -4,11 +4,15 @@
|
|||||||
<h2 class="page-title">租户管理</h2>
|
<h2 class="page-title">租户管理</h2>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<el-button type="primary" @click="handleAdd">
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon>
|
||||||
|
<Plus />
|
||||||
|
</el-icon>
|
||||||
添加租户
|
添加租户
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="refresh" :loading="loading">
|
<el-button @click="refresh" :loading="loading">
|
||||||
<el-icon><Refresh /></el-icon>
|
<el-icon>
|
||||||
|
<Refresh />
|
||||||
|
</el-icon>
|
||||||
刷新
|
刷新
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -32,17 +36,28 @@
|
|||||||
<!-- 租户列表 -->
|
<!-- 租户列表 -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
<el-table
|
<el-table :data="tenants" stripe style="width: 100%" v-loading="loading" element-loading-text="正在加载...">
|
||||||
:data="tenants"
|
|
||||||
stripe
|
|
||||||
style="width: 100%"
|
|
||||||
v-loading="loading"
|
|
||||||
element-loading-text="正在加载..."
|
|
||||||
>
|
|
||||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||||
<el-table-column prop="name" label="租户名称" min-width="120" />
|
<el-table-column prop="name" label="租户名称" min-width="240" />
|
||||||
<el-table-column prop="code" label="租户编码" width="120" />
|
|
||||||
<el-table-column prop="owner" label="负责人" width="100" />
|
<el-table-column prop="owner" label="负责人" width="100" />
|
||||||
|
<el-table-column label="审核状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="reviewStatusDict.length" :type="getAuditStatusType(row.audit_status)"
|
||||||
|
:class="`${row.audit_status}-color`">
|
||||||
|
{{ getAuditStatusText(row.audit_status) }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="tenantStatusDict.length" :type="getTenantStatusType(row.status)"
|
||||||
|
:class="`color-${row.status}`">
|
||||||
|
{{ getTenantStatusText(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="存储容量" min-width="240">
|
<el-table-column label="存储容量" min-width="240">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="capacity-cell">
|
<div class="capacity-cell">
|
||||||
@ -51,60 +66,41 @@
|
|||||||
<span class="capacity-divider">/</span>
|
<span class="capacity-divider">/</span>
|
||||||
<span>{{ formatCapacity(row.capacity ?? 0) }}</span>
|
<span>{{ formatCapacity(row.capacity ?? 0) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-progress
|
<el-progress :percentage="getCapacityPercentage(row.capacity, row.capacity_used)"
|
||||||
:percentage="getCapacityPercentage(row.capacity, row.capacity_used)"
|
:color="getCapacityColor(row.capacity, row.capacity_used)" :stroke-width="4"
|
||||||
:color="getCapacityColor(row.capacity, row.capacity_used)"
|
style="margin-top: 4px;" />
|
||||||
:stroke-width="4"
|
|
||||||
style="margin-top: 4px;"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="created_at" label="创建时间" width="150" />
|
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||||
<el-table-column label="审核状态" width="100">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="getAuditStatusType(row.audit_status)">
|
{{ formatTime(row.created_at) }}
|
||||||
{{ getAuditStatusText(row.audit_status) }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="状态" width="80">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="getTenantStatusType(row.status)">
|
|
||||||
{{ getTenantStatusText(row.status) }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="300" align="center" fixed="right">
|
<el-table-column label="操作" width="300" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button size="small" @click="handleView(row)">
|
<el-button size="small" @click="handleView(row)">
|
||||||
<el-icon><View /></el-icon>
|
<el-icon>
|
||||||
|
<View />
|
||||||
|
</el-icon>
|
||||||
查看
|
查看
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button size="small" type="success" v-if="row.audit_status === 'pending'" @click="handleAudit(row)">
|
||||||
size="small"
|
<el-icon>
|
||||||
type="success"
|
<Check />
|
||||||
v-if="row.audit_status === 'pending'"
|
</el-icon>
|
||||||
@click="handleAudit(row)"
|
|
||||||
>
|
|
||||||
<el-icon><Check /></el-icon>
|
|
||||||
审核
|
审核
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button size="small" @click="handleEdit(row)" v-if="row.audit_status !== 'approved'">
|
||||||
size="small"
|
<el-icon>
|
||||||
@click="handleEdit(row)"
|
<Edit />
|
||||||
v-if="row.audit_status !== 'approved'"
|
</el-icon>
|
||||||
>
|
|
||||||
<el-icon><Edit /></el-icon>
|
|
||||||
编辑
|
编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button size="small" type="danger" @click="handleDelete(row)" v-if="row.audit_status !== 'approved'">
|
||||||
size="small"
|
<el-icon>
|
||||||
type="danger"
|
<Delete />
|
||||||
@click="handleDelete(row)"
|
</el-icon>
|
||||||
v-if="row.audit_status !== 'approved'"
|
|
||||||
>
|
|
||||||
<el-icon><Delete /></el-icon>
|
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
@ -113,36 +109,19 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<div class="pagination-wrapper">
|
<div class="pagination-wrapper">
|
||||||
<el-pagination
|
<el-pagination background :current-page="page" :page-size="pageSize" :total="total"
|
||||||
background
|
@current-change="handlePageChange" layout="total, prev, pager, next" />
|
||||||
:current-page="page"
|
|
||||||
:page-size="pageSize"
|
|
||||||
:total="total"
|
|
||||||
@current-change="handlePageChange"
|
|
||||||
layout="total, prev, pager, next"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 详情组件 -->
|
<!-- 详情组件 -->
|
||||||
<TenantDetail
|
<TenantDetail v-model="showViewDialog" :tenant-id="currentTenant?.id" />
|
||||||
v-model="showViewDialog"
|
|
||||||
:tenant-id="currentTenant?.id"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 审核组件 -->
|
<!-- 审核组件 -->
|
||||||
<TenantAudit
|
<TenantAudit v-model="showAuditDialog" :tenant="currentTenant" @success="handleAuditSuccess" />
|
||||||
v-model="showAuditDialog"
|
|
||||||
:tenant="currentTenant"
|
|
||||||
@success="handleAuditSuccess"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 编辑组件 -->
|
<!-- 编辑组件 -->
|
||||||
<TenantEdit
|
<TenantEdit v-model="showEditDialog" :tenant="currentTenant" @success="handleEditSuccess" />
|
||||||
v-model="showEditDialog"
|
|
||||||
:tenant="currentTenant"
|
|
||||||
@success="handleEditSuccess"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -155,6 +134,7 @@ import { useDictStore } from '@/stores/dict';
|
|||||||
import TenantDetail from './components/detail.vue';
|
import TenantDetail from './components/detail.vue';
|
||||||
import TenantAudit from './components/audit.vue';
|
import TenantAudit from './components/audit.vue';
|
||||||
import TenantEdit from './components/edit.vue';
|
import TenantEdit from './components/edit.vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
// 字典数据
|
// 字典数据
|
||||||
const reviewStatusDict = ref<any[]>([]);
|
const reviewStatusDict = ref<any[]>([]);
|
||||||
@ -229,6 +209,12 @@ const handlePageChange = (val: number) => {
|
|||||||
fetchTenants();
|
fetchTenants();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//格式化时间
|
||||||
|
function formatTime(time: string | null | undefined) {
|
||||||
|
if (!time) return '';
|
||||||
|
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
}
|
||||||
|
|
||||||
// 刷新界面
|
// 刷新界面
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
try {
|
try {
|
||||||
@ -250,14 +236,14 @@ function getAuditStatusType(
|
|||||||
if (dictItem) {
|
if (dictItem) {
|
||||||
return dictItem.dict_tag_type || 'info';
|
return dictItem.dict_tag_type || 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底方案,保持原有逻辑
|
// 兜底方案,保持原有逻辑
|
||||||
const statusMap: Record<string, 'warning' | 'success' | 'danger' | 'info'> =
|
const statusMap: Record<string, 'warning' | 'success' | 'danger' | 'info'> =
|
||||||
{
|
{
|
||||||
pending: 'warning',
|
pending: 'warning',
|
||||||
approved: 'success',
|
approved: 'success',
|
||||||
rejected: 'danger',
|
rejected: 'danger',
|
||||||
};
|
};
|
||||||
return statusMap[status] || 'info';
|
return statusMap[status] || 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,7 +255,7 @@ function getAuditStatusText(status: string) {
|
|||||||
if (dictItem) {
|
if (dictItem) {
|
||||||
return dictItem.dict_label || String(status);
|
return dictItem.dict_label || String(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底方案,保持原有逻辑
|
// 兜底方案,保持原有逻辑
|
||||||
const statusMap: Record<string, string> = {
|
const statusMap: Record<string, string> = {
|
||||||
pending: '待审核',
|
pending: '待审核',
|
||||||
@ -288,7 +274,7 @@ function getTenantStatusType(status: string | number): 'success' | 'info' | 'war
|
|||||||
if (dictItem) {
|
if (dictItem) {
|
||||||
return dictItem.dict_tag_type || 'info';
|
return dictItem.dict_tag_type || 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底方案,保持原有逻辑
|
// 兜底方案,保持原有逻辑
|
||||||
return String(status) === '1' ? 'success' : 'info';
|
return String(status) === '1' ? 'success' : 'info';
|
||||||
}
|
}
|
||||||
@ -301,7 +287,7 @@ function getTenantStatusText(status: string | number): string {
|
|||||||
if (dictItem) {
|
if (dictItem) {
|
||||||
return dictItem.dict_label || String(status);
|
return dictItem.dict_label || String(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底方案,保持原有逻辑
|
// 兜底方案,保持原有逻辑
|
||||||
return String(status) === '1' ? '启用' : '禁用';
|
return String(status) === '1' ? '启用' : '禁用';
|
||||||
}
|
}
|
||||||
@ -471,5 +457,23 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
.pending-color {
|
||||||
|
color: #e6a23c;
|
||||||
|
background-color: #fdf6ec;
|
||||||
|
border-color: #f5dab1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approved-color,
|
||||||
|
.color-1 {
|
||||||
|
color: #67c23a;
|
||||||
|
background-color: rgb(240, 249, 235);
|
||||||
|
border-color: rgb(225, 243, 216);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rejected-color {
|
||||||
|
color: #f56c6c;
|
||||||
|
background-color: #fef0f0;
|
||||||
|
border-color: #fbc4c4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -304,3 +304,47 @@ func (c *TaskController) DeleteTask() {
|
|||||||
}
|
}
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 仪表盘获取待办任务
|
||||||
|
// GetTodoTasks 获取待办任务列表
|
||||||
|
// @router /api/oa/tasks/todo [get]
|
||||||
|
func (c *TaskController) GetTodoTasks() {
|
||||||
|
// 优先取查询参数,其次取中间件写入的上下文
|
||||||
|
tenantId, _ := c.GetInt("tenantId", 0)
|
||||||
|
if tenantId == 0 {
|
||||||
|
if v := c.Ctx.Input.GetData("tenantId"); v != nil {
|
||||||
|
if tid, ok := v.(int); ok {
|
||||||
|
tenantId = tid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id, _ := c.GetInt("id", 0)
|
||||||
|
if id == 0 {
|
||||||
|
if v := c.Ctx.Input.GetData("id"); v != nil {
|
||||||
|
if uid, ok := v.(int); ok {
|
||||||
|
id = uid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
limit, _ := c.GetInt("limit", 10)
|
||||||
|
|
||||||
|
tasks, err := services.GetTodoTasks(tenantId, id, limit)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 1,
|
||||||
|
"message": "获取待办任务失败: " + err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 0,
|
||||||
|
"message": "获取成功",
|
||||||
|
"data": tasks,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
|||||||
20
server/database/sys_feedback.sql
Normal file
20
server/database/sys_feedback.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
-- 反馈表:sys_feedback(修复TEXT字段默认值错误)
|
||||||
|
CREATE TABLE `sys_feedback` (
|
||||||
|
`id` varchar(36) NOT NULL COMMENT 'ID',
|
||||||
|
`tenant_id` varchar(64) NOT NULL COMMENT '租户ID',
|
||||||
|
`feedback_name` varchar(50) DEFAULT '' COMMENT '反馈人姓名',
|
||||||
|
`module` varchar(30) NOT NULL COMMENT '反馈对应模块',
|
||||||
|
`feedback_type` varchar(20) NOT NULL COMMENT '反馈类型',
|
||||||
|
`content` text NOT NULL COMMENT '反馈详细内容',
|
||||||
|
`attachment_url` varchar(255) DEFAULT '' COMMENT '附件URL',
|
||||||
|
`handle_status` varchar(20) NOT NULL DEFAULT '0' COMMENT '处理状态(0-待处理/1-处理中/2-已解决/3-已驳回/4-无需处理)',
|
||||||
|
`handle_remark` text COMMENT '处理备注(移除默认值,TEXT类型不支持)',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_tenant_id` (`tenant_id`) COMMENT '租户ID索引,优化租户级查询',
|
||||||
|
KEY `idx_module` (`module`) COMMENT '模块索引,优化模块级反馈统计',
|
||||||
|
KEY `idx_handle_status` (`handle_status`) COMMENT '处理状态索引,优化待处理反馈查询',
|
||||||
|
KEY `idx_create_time` (`create_time`) COMMENT '创建时间索引,优化时间范围查询'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通用反馈表(支持租户隔离、软删除)';
|
||||||
@ -22,5 +22,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 静态资源:映射 /uploads 到本地 uploads 目录,供前端访问上传文件
|
||||||
|
beego.SetStaticPath("/uploads", "uploads")
|
||||||
|
|
||||||
beego.Run()
|
beego.Run()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,6 +48,10 @@ func (f *FileInfo) TableName() string {
|
|||||||
return "yz_files"
|
return "yz_files"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Removed duplicate ORM registration
|
||||||
|
}
|
||||||
|
|
||||||
// CanPreview 判断文件是否可以在线预览
|
// CanPreview 判断文件是否可以在线预览
|
||||||
func (f *FileInfo) CanPreview() bool {
|
func (f *FileInfo) CanPreview() bool {
|
||||||
previewableExts := map[string]bool{
|
previewableExts := map[string]bool{
|
||||||
|
|||||||
@ -121,3 +121,18 @@ func UpdateTask(t *Task, cols ...string) error {
|
|||||||
_, err := o.Update(t, cols...)
|
_, err := o.Update(t, cols...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTodoTasks 获取待办任务列表
|
||||||
|
func GetTodoTasks(tenantId int, id int, limit int) (tasks []*Task, err error) {
|
||||||
|
o := orm.NewOrm()
|
||||||
|
qs := o.QueryTable(new(Task)).Filter("tenant_id", tenantId).Filter("deleted_time__isnull", true)
|
||||||
|
// 如果传了用户,则筛选负责人为该用户的任务
|
||||||
|
if id > 0 {
|
||||||
|
qs = qs.Filter("principal_id", id)
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
_, err = qs.OrderBy("-id").Limit(limit).All(&tasks)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@ -8,25 +8,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Tenant struct {
|
type Tenant struct {
|
||||||
Id int `orm:"pk;auto" json:"id"`
|
Id int `orm:"pk;auto" json:"id"`
|
||||||
Name string `orm:"size(100)" json:"name"`
|
Name string `orm:"size(100)" json:"name"`
|
||||||
Code string `orm:"size(50);unique" json:"code"`
|
Code string `orm:"size(50);unique" json:"code"`
|
||||||
Owner string `orm:"size(50)" json:"owner"`
|
Owner string `orm:"size(50)" json:"owner"`
|
||||||
Phone string `orm:"size(20);null" json:"phone"`
|
Phone string `orm:"size(20);null" json:"phone"`
|
||||||
Email string `orm:"size(100);null" json:"email"`
|
Email string `orm:"size(100);null" json:"email"`
|
||||||
Status int `orm:"default(1)" json:"status"`
|
Status int `orm:"default(1)" json:"status"`
|
||||||
AuditStatus string `orm:"size(20);default(pending)" json:"audit_status"`
|
AuditStatus string `orm:"size(20);default(pending)" json:"audit_status"`
|
||||||
AuditComment string `orm:"type(text);null" json:"audit_comment"`
|
AuditComment string `orm:"type(text);null" json:"audit_comment"`
|
||||||
AuditBy string `orm:"size(50);null" json:"audit_by"`
|
AuditBy string `orm:"size(50);null" json:"audit_by"`
|
||||||
AuditTime *time.Time `orm:"null;type(datetime)" json:"audit_time"`
|
AuditTime *time.Time `orm:"null;type(datetime)" json:"audit_time"`
|
||||||
Capacity int `orm:"default(0)" json:"capacity"`
|
Capacity int `orm:"default(0)" json:"capacity"`
|
||||||
CapacityUsed int `orm:"default(0)" json:"capacity_used"`
|
CapacityUsed int `orm:"default(0)" json:"capacity_used"`
|
||||||
Remark string `orm:"type(text);null" json:"remark"`
|
AttachmentUrl string `orm:"size(255);null" json:"attachment_url"`
|
||||||
CreateTime time.Time `orm:"auto_now_add;type(datetime)" json:"create_time"`
|
Remark string `orm:"type(text);null" json:"remark"`
|
||||||
UpdateTime time.Time `orm:"auto_now;type(datetime)" json:"update_time"`
|
CreateTime time.Time `orm:"auto_now_add;type(datetime)" json:"create_time"`
|
||||||
DeleteTime *time.Time `orm:"null;type(datetime)" json:"delete_time"`
|
UpdateTime time.Time `orm:"auto_now;type(datetime)" json:"update_time"`
|
||||||
CreateBy string `orm:"size(50);null" json:"create_by"`
|
DeleteTime *time.Time `orm:"null;type(datetime)" json:"delete_time"`
|
||||||
UpdateBy string `orm:"size(50);null" json:"update_by"`
|
CreateBy string `orm:"size(50);null" json:"create_by"`
|
||||||
|
UpdateBy string `orm:"size(50);null" json:"update_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 设置表名
|
// TableName 设置表名
|
||||||
|
|||||||
@ -270,6 +270,8 @@ func init() {
|
|||||||
// 文件管理路由 - 手动配置以匹配前端的 /api/files 路径
|
// 文件管理路由 - 手动配置以匹配前端的 /api/files 路径
|
||||||
beego.Router("/api/files", &controllers.FileController{}, "get:GetAllFiles")
|
beego.Router("/api/files", &controllers.FileController{}, "get:GetAllFiles")
|
||||||
beego.Router("/api/files", &controllers.FileController{}, "post:Post")
|
beego.Router("/api/files", &controllers.FileController{}, "post:Post")
|
||||||
|
// 兼容前端上传地址 /api/files/upload -> 复用 Post 处理上传
|
||||||
|
beego.Router("/api/files/upload", &controllers.FileController{}, "post:Post")
|
||||||
beego.Router("/api/files/my", &controllers.FileController{}, "get:GetMyFiles")
|
beego.Router("/api/files/my", &controllers.FileController{}, "get:GetMyFiles")
|
||||||
beego.Router("/api/files/download/:id", &controllers.FileController{}, "get:DownloadFile")
|
beego.Router("/api/files/download/:id", &controllers.FileController{}, "get:DownloadFile")
|
||||||
beego.Router("/api/files/preview/:id", &controllers.FileController{}, "get:PreviewFile")
|
beego.Router("/api/files/preview/:id", &controllers.FileController{}, "get:PreviewFile")
|
||||||
@ -315,6 +317,7 @@ func init() {
|
|||||||
// OA任务管理路由
|
// OA任务管理路由
|
||||||
beego.Router("/api/oa/tasks", &controllers.TaskController{}, "get:GetTasks;post:CreateTask")
|
beego.Router("/api/oa/tasks", &controllers.TaskController{}, "get:GetTasks;post:CreateTask")
|
||||||
beego.Router("/api/oa/tasks/:id", &controllers.TaskController{}, "get:GetTaskById;put:UpdateTask;delete:DeleteTask")
|
beego.Router("/api/oa/tasks/:id", &controllers.TaskController{}, "get:GetTaskById;put:UpdateTask;delete:DeleteTask")
|
||||||
|
beego.Router("/api/oa/tasks/todo", &controllers.TaskController{}, "get:GetTodoTasks")
|
||||||
|
|
||||||
// 权限管理路由
|
// 权限管理路由
|
||||||
beego.Router("/api/permissions/menus", &controllers.PermissionController{}, "get:GetAllMenuPermissions")
|
beego.Router("/api/permissions/menus", &controllers.PermissionController{}, "get:GetAllMenuPermissions")
|
||||||
|
|||||||
@ -69,3 +69,8 @@ func DeleteOATask(id int, operatorName string, operatorId int) error {
|
|||||||
func genTaskNo(tenantId int) string {
|
func genTaskNo(tenantId int) string {
|
||||||
return fmt.Sprintf("TASK%d%s", tenantId, time.Now().Format("20060102150405"))
|
return fmt.Sprintf("TASK%d%s", tenantId, time.Now().Format("20060102150405"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 仪表盘获取待办任务
|
||||||
|
func GetTodoTasks(tenantId int, id int, limit int) (tasks []*models.Task, err error) {
|
||||||
|
return models.GetTodoTasks(tenantId, id, limit)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user