优化仪表盘等
This commit is contained in:
parent
db16ee70de
commit
117e2b3440
@ -1,5 +1,10 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取任务列表
|
||||
* @param {Object} params 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function listTasks(params) {
|
||||
return request({
|
||||
url: '/api/oa/tasks',
|
||||
@ -8,6 +13,11 @@ export function listTasks(params) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务详情
|
||||
* @param {number|string} id 任务ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getTask(id) {
|
||||
return request({
|
||||
url: `/api/oa/tasks/${id}`,
|
||||
@ -15,6 +25,11 @@ export function getTask(id) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建任务
|
||||
* @param {Object} data 任务数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function createTask(data) {
|
||||
return request({
|
||||
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) {
|
||||
return request({
|
||||
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) {
|
||||
return request({
|
||||
url: `/api/oa/tasks/${id}`,
|
||||
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>
|
||||
</el-button>
|
||||
<el-breadcrumb separator="/" class="bread">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item
|
||||
v-for="(item, index) in breadcrumbs"
|
||||
:key="index"
|
||||
:to="(item.label === '首页' || index === breadcrumbs.length - 1) ? { path: item.path } : null"
|
||||
>
|
||||
<el-breadcrumb-item :to="{ path: '/' }" style="position: relative !important; top: 1px !important;">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="index"
|
||||
:to="(item.label === '首页' || index === breadcrumbs.length - 1) ? { path: item.path } : null" style="position: relative !important; top: 1px !important;">
|
||||
{{ item.label }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="r-content">
|
||||
<!-- 更新缓存按钮 -->
|
||||
<el-button
|
||||
circle
|
||||
:icon="Refresh"
|
||||
@click="refreshCache"
|
||||
class="refresh-cache-btn"
|
||||
:loading="cacheLoading"
|
||||
title="更新菜单缓存"
|
||||
/>
|
||||
|
||||
<el-button circle :icon="Refresh" @click="refreshCache" class="refresh-cache-btn" :loading="cacheLoading"
|
||||
title="更新菜单缓存" />
|
||||
|
||||
<!-- 主题切换按钮 -->
|
||||
<el-button
|
||||
circle
|
||||
:icon="themeIcon"
|
||||
@click="toggleTheme"
|
||||
class="theme-toggle-btn"
|
||||
:title="currentTheme === 'dark' ? '切换到亮色模式' : '切换到暗色模式'"
|
||||
/>
|
||||
|
||||
<el-button circle :icon="themeIcon" @click="toggleTheme" class="theme-toggle-btn"
|
||||
:title="currentTheme === 'dark' ? '切换到亮色模式' : '切换到暗色模式'" />
|
||||
|
||||
<el-dropdown trigger="click" @command="handleCommand">
|
||||
<span class="el-dropdown-link" style="cursor: pointer;">
|
||||
<img :src="getImageUrl('user')" class="user" />
|
||||
@ -43,11 +29,15 @@
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="profile">
|
||||
<el-icon><User /></el-icon>
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
<span>个人中心</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
<el-icon>
|
||||
<SwitchButton />
|
||||
</el-icon>
|
||||
<span>退出登录</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@ -56,13 +46,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { useAllDataStore, useMenuStore } from "@/stores";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
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 route = useRoute();
|
||||
@ -95,19 +86,19 @@ async function refreshCache() {
|
||||
cacheLoading.value = true;
|
||||
try {
|
||||
await menuStore.refreshMenus();
|
||||
|
||||
|
||||
// 重新加载动态路由
|
||||
const { loadAndAddDynamicRoutes, resetDynamicRoutes } = await import('@/router/index');
|
||||
// 重置路由加载状态,强制重新加载
|
||||
resetDynamicRoutes();
|
||||
await loadAndAddDynamicRoutes();
|
||||
|
||||
|
||||
// 等待路由完全加载(给Vue Router一些时间更新路由表)
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
|
||||
// 触发菜单刷新事件,通知CommonAside组件刷新菜单
|
||||
window.dispatchEvent(new CustomEvent('menu-cache-refreshed'));
|
||||
|
||||
|
||||
ElMessage.success('更新成功');
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh cache', error);
|
||||
@ -117,14 +108,14 @@ async function refreshCache() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadMenu);
|
||||
onMounted(loadMenu);
|
||||
|
||||
// 根据菜单列表和当前路径计算出的面包屑导航
|
||||
const breadcrumbs = computed(() => {
|
||||
let chain: Breadcrumb[] = [];
|
||||
let currentPath = route.path || '/';
|
||||
if (currentPath === '/' || currentPath === '') {
|
||||
return [{ label: '仪表盘', path: '/' }];
|
||||
return [{ label: '仪表盘', path: '/' }];
|
||||
}
|
||||
let current = menuList.value.find(m => m.path === currentPath);
|
||||
if (!current) {
|
||||
@ -141,8 +132,8 @@ const breadcrumbs = computed(() => {
|
||||
parentId = parent.parentId;
|
||||
} else break;
|
||||
}
|
||||
chain = chain.reverse();
|
||||
return chain;
|
||||
chain = chain.reverse();
|
||||
return chain;
|
||||
});
|
||||
|
||||
const store = useAllDataStore();
|
||||
@ -156,17 +147,17 @@ const getImageUrl = (user) => {
|
||||
const displayName = computed(() => {
|
||||
const user = authStore.user;
|
||||
if (!user) return '';
|
||||
|
||||
|
||||
// 如果是员工登录,优先显示name
|
||||
if (user.type === 'employee' && user.name) {
|
||||
return user.name;
|
||||
}
|
||||
|
||||
|
||||
// 如果是用户登录,优先显示nickname
|
||||
if (user.nickname) {
|
||||
return user.nickname;
|
||||
}
|
||||
|
||||
|
||||
// 最后显示username
|
||||
return user.username || '';
|
||||
});
|
||||
@ -192,7 +183,7 @@ const handleCommand = async (command) => {
|
||||
sessionStorage.removeItem('tabs_list');
|
||||
// 清除菜单缓存
|
||||
menuStore.resetMenus();
|
||||
|
||||
|
||||
// 清除所有以 menu_cache_ 开头的本地存储项
|
||||
const menuCacheKeys: string[] = [];
|
||||
// 遍历 localStorage
|
||||
@ -206,7 +197,7 @@ const handleCommand = async (command) => {
|
||||
menuCacheKeys.forEach(key => {
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
|
||||
|
||||
// 遍历 sessionStorage
|
||||
const sessionMenuCacheKeys: string[] = [];
|
||||
for (let i = 0; i < sessionStorage.length; i++) {
|
||||
@ -219,12 +210,12 @@ const handleCommand = async (command) => {
|
||||
sessionMenuCacheKeys.forEach(key => {
|
||||
sessionStorage.removeItem(key);
|
||||
});
|
||||
|
||||
|
||||
// 重置 tabs store 状态
|
||||
const { useTabsStore } = await import('@/stores');
|
||||
const tabsStore = useTabsStore();
|
||||
tabsStore.resetTabs();
|
||||
|
||||
|
||||
router.push('/login');
|
||||
}
|
||||
};
|
||||
@ -276,7 +267,7 @@ let handleChange: ((e: MediaQueryListEvent) => void) | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
initTheme();
|
||||
|
||||
|
||||
// 监听系统主题变化
|
||||
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
handleChange = (e: MediaQueryListEvent) => {
|
||||
@ -310,7 +301,7 @@ onUnmounted(() => {
|
||||
color: var(--el-text-color-primary);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
|
||||
|
||||
// 亮色主题下使用 #062da3 背景
|
||||
html:not(.dark) & {
|
||||
background-color: #062da3;
|
||||
@ -333,7 +324,7 @@ onUnmounted(() => {
|
||||
background-color: var(--el-fill-color-light);
|
||||
border-color: var(--el-border-color);
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color);
|
||||
border-color: var(--el-border-color-dark);
|
||||
@ -348,7 +339,7 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
|
||||
.refresh-cache-btn,
|
||||
.theme-toggle-btn {
|
||||
// 使用 Element Plus 的填充色变量
|
||||
@ -356,14 +347,14 @@ onUnmounted(() => {
|
||||
border-color: var(--el-border-color);
|
||||
color: var(--el-text-color-primary);
|
||||
margin-left: 0 !important;
|
||||
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color);
|
||||
border-color: var(--el-border-color-dark);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.user {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@ -371,18 +362,18 @@ onUnmounted(() => {
|
||||
// 使用 Element Plus 的边框颜色变量
|
||||
border: 2px solid var(--el-border-color);
|
||||
transition: border-color 0.3s ease;
|
||||
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
gap: 12px;
|
||||
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
@ -391,7 +382,7 @@ onUnmounted(() => {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
|
||||
// 亮色主题下使用白色
|
||||
html:not(.dark) & {
|
||||
color: #ffffff;
|
||||
@ -407,27 +398,27 @@ onUnmounted(() => {
|
||||
background-color: var(--bg-color-overlay) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
padding: 4px 0;
|
||||
|
||||
|
||||
.el-dropdown-menu__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-color-primary) !important;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
|
||||
|
||||
span {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
|
||||
.el-icon {
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
|
||||
&:not(.is-disabled):hover {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
|
||||
&.is-divided {
|
||||
border-top-color: var(--border-color) !important;
|
||||
}
|
||||
@ -437,18 +428,23 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
:deep(.bread) {
|
||||
|
||||
// 面包屑使用白色
|
||||
.el-breadcrumb__inner {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
|
||||
.el-breadcrumb__inner.is-link {
|
||||
color: #ffffff !important;
|
||||
cursor: pointer !important;
|
||||
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav){
|
||||
height: 36px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -9,6 +9,22 @@
|
||||
<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">{{ 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>
|
||||
<template #footer>
|
||||
<el-button @click="visible=false">关闭</el-button>
|
||||
@ -19,6 +35,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean, task: any }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
@ -39,7 +56,51 @@ const formatDateTime = (v: any) => {
|
||||
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())}`
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
@ -46,6 +46,25 @@
|
||||
<el-form-item label="描述" prop="task_desc">
|
||||
<el-input v-model="form.task_desc" type="textarea" :rows="4" />
|
||||
</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>
|
||||
<EmployeeSelectDialog
|
||||
v-model:visible="employeeDialogVisible"
|
||||
@ -72,6 +91,9 @@ import { useDictStore } from '@/stores/dict'
|
||||
import EmployeeSelectDialog from './EmployeeSelectDialog.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
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<{
|
||||
modelValue: boolean,
|
||||
@ -105,6 +127,22 @@ const form = reactive<any>({
|
||||
// 关联人(仅提交 id 数组在 team_employee_ids)
|
||||
const teamEmployeeIds = ref<Array<number | 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 loadTeamEmployeeNames = async (ids: Array<number | string>) => {
|
||||
if (!ids || ids.length === 0) {
|
||||
@ -183,6 +221,16 @@ watch(() => props.task, (t) => {
|
||||
} else {
|
||||
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 })
|
||||
|
||||
const rules = reactive<FormRules<any>>({
|
||||
@ -239,6 +287,14 @@ const onSubmit = async () => {
|
||||
const n = Number(x)
|
||||
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) {
|
||||
res = await updateTask(form.id, payload)
|
||||
@ -264,6 +320,63 @@ const onSubmit = async () => {
|
||||
const onClosed = () => {
|
||||
// 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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<template #default="{ row }">
|
||||
<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="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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -87,6 +87,7 @@ import { listTasks, getTask, deleteTask } from '@/api/tasks'
|
||||
import Edit from './components/edit.vue'
|
||||
import Detail from './components/detail.vue'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
@ -110,6 +111,8 @@ const currentTask = ref<any>(null)
|
||||
const detailVisible = ref(false)
|
||||
const deleteDialogVisible = ref(false) // 保留变量但不渲染独立组件
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@ -179,6 +182,13 @@ const openDelete = (row: any) => {
|
||||
.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) => {
|
||||
if (v === 'urgent') return 'danger'
|
||||
if (v === 'high') return 'warning'
|
||||
|
||||
@ -356,6 +356,7 @@ import { useRouter } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { getKnowledgeList } from "@/api/knowledge";
|
||||
import { getTodoTasks } from "@/api/tasks";
|
||||
import {
|
||||
FolderOpened,
|
||||
StarFilled,
|
||||
@ -605,78 +606,24 @@ const industryNews = ref([
|
||||
const sortType = ref("time");
|
||||
|
||||
// 待办事项
|
||||
const tasks = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: "设计部李亦想的加班申请流程",
|
||||
node: "直属领导审批",
|
||||
applyTime: "2024-01-20 13:32",
|
||||
system: "oa",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "财务部张申请的报销流程",
|
||||
node: "财务审核",
|
||||
applyTime: "2024-01-19 10:15",
|
||||
system: "finance",
|
||||
},
|
||||
{
|
||||
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 tasks = ref([]);
|
||||
|
||||
async function fetchTodoTasks() {
|
||||
try {
|
||||
const tenantId = authStore?.user?.tenant_id || 0;
|
||||
const res = await getTodoTasks({ tenantId, limit: 10 });
|
||||
const list = res?.data?.list || res?.data || [];
|
||||
tasks.value = list.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.task_name || item.title || "",
|
||||
node: item.node || item.task_status || "",
|
||||
applyTime: item.apply_time || item.created_time || item.createdAt || "",
|
||||
system: item.system || "oa",
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error("获取待办任务失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
return tasks.value;
|
||||
@ -885,6 +832,8 @@ onMounted(() => {
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
// 加载知识库数据
|
||||
fetchKnowledgeList();
|
||||
// 加载我的待办
|
||||
fetchTodoTasks();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@ -93,16 +93,19 @@
|
||||
</div>
|
||||
<div class="list-content">
|
||||
<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-title">
|
||||
{{ task.title }}
|
||||
<el-tag :type="getPriorityType(task.priority)" size="small" effect="plain">
|
||||
{{ task.priority }}
|
||||
<el-tag :type="task.priorityTagType || getPriorityType(task.priority)" size="small" effect="plain" :class="'priority-' + task.priority">
|
||||
{{ task.priorityLabel || task.priority }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<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>
|
||||
@ -164,8 +167,10 @@ import {
|
||||
View,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { getKnowledgeCount } from "@/api/knowledge";
|
||||
import { getTodoTasks } from "@/api/tasks";
|
||||
import { getPlatformStats, getTenantStats, getActivityLogs } from "@/api/dashboard";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
Chart.register(...registerables);
|
||||
|
||||
@ -179,6 +184,14 @@ const currentDate = computed(() => {
|
||||
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 避免图标组件被响应式化)
|
||||
const stats = ref([
|
||||
{
|
||||
@ -288,56 +301,48 @@ const fetchTenantStats = async () => {
|
||||
const taskCurrentPage = ref(1);
|
||||
const taskPageSize = ref(5);
|
||||
|
||||
const tasks = ref([
|
||||
{
|
||||
title: "完成Q2预算审核",
|
||||
date: "2024-06-11",
|
||||
priority: "High",
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
title: "发邮件通知团队",
|
||||
date: "2024-06-10",
|
||||
priority: "Medium",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
title: "产品需求讨论会",
|
||||
date: "2024-06-09",
|
||||
priority: "Low",
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
title: "准备季度报告",
|
||||
date: "2024-06-12",
|
||||
priority: "High",
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
title: "更新项目文档",
|
||||
date: "2024-06-13",
|
||||
priority: "Medium",
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
title: "代码审查",
|
||||
date: "2024-06-14",
|
||||
priority: "Low",
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
title: "客户需求沟通",
|
||||
date: "2024-06-15",
|
||||
priority: "High",
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
title: "测试环境部署",
|
||||
date: "2024-06-16",
|
||||
priority: "Medium",
|
||||
completed: false,
|
||||
},
|
||||
]);
|
||||
const tasks = ref<any[]>([]);
|
||||
const priorityDict = ref<any[]>([]);
|
||||
|
||||
async function fetchPriorityDict() {
|
||||
try {
|
||||
const dictStore = useDictStore();
|
||||
priorityDict.value = await dictStore.getDictItems('task_priority');
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTodoTasksDashboard() {
|
||||
try {
|
||||
const tenantId = authStore?.user?.tenant_id || 0;
|
||||
const id = authStore?.user?.id || 0;
|
||||
const res = await getTodoTasks({ tenantId, id, limit: 20 });
|
||||
const list = res?.data?.list || res?.data || [];
|
||||
// 映射到仪表盘展示所需结构
|
||||
tasks.value = list.map((item: any) => ({
|
||||
title: item.task_name || item.title || "",
|
||||
date: item.created_time || item.apply_time || item.updated_time || "",
|
||||
priority: item.priority || "",
|
||||
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');
|
||||
})(),
|
||||
priorityTagType: (() => {
|
||||
const dictItem = priorityDict.value.find((d: any) => String(d.dict_value) === String(item.priority));
|
||||
return dictItem?.dict_tag_type || undefined;
|
||||
})(),
|
||||
completed: item.task_status === 'completed' || item.task_status === 'closed',
|
||||
}));
|
||||
} catch (e) {
|
||||
console.warn('获取待办任务失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
function capitalize(s: string) {
|
||||
if (!s) return s;
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
// 分页后的任务列表
|
||||
const paginatedTasks = computed(() => {
|
||||
@ -445,130 +450,128 @@ onMounted(() => {
|
||||
// 加载活动日志
|
||||
fetchActivityLogs();
|
||||
|
||||
// 加载待办任务
|
||||
fetchPriorityDict();
|
||||
fetchTodoTasksDashboard();
|
||||
|
||||
// 折线图
|
||||
const lineChartEl = document.getElementById("lineChart") as HTMLCanvasElement | null;
|
||||
if (!lineChartEl) {
|
||||
console.error("Line chart element not found");
|
||||
return;
|
||||
if (lineChartEl) {
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
if (!barChartEl) {
|
||||
console.error("Bar chart element not found");
|
||||
return;
|
||||
if (barChartEl) {
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@ -822,6 +825,7 @@ onMounted(() => {
|
||||
|
||||
&.done {
|
||||
.task-info {
|
||||
margin-left: 20px;
|
||||
.task-title {
|
||||
text-decoration: line-through;
|
||||
color: var(--el-text-color-placeholder);
|
||||
@ -978,4 +982,22 @@ onMounted(() => {
|
||||
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>
|
||||
|
||||
@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
title="租户详情"
|
||||
v-model="visible"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-dialog title="租户详情" v-model="visible" width="600px" :close-on-click-modal="false" @close="handleClose">
|
||||
<div class="tenant-detail" v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="租户ID">
|
||||
@ -14,9 +8,6 @@
|
||||
<el-descriptions-item label="租户名称">
|
||||
{{ tenantData.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="租户编码">
|
||||
{{ tenantData.code }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">
|
||||
{{ tenantData.owner }}
|
||||
</el-descriptions-item>
|
||||
@ -27,12 +18,12 @@
|
||||
{{ tenantData.email || '未设置' }}
|
||||
</el-descriptions-item>
|
||||
<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) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getTenantStatusType(tenantData.status)">
|
||||
<el-tag :type="getTenantStatusType(tenantData.status)" :class="`color-${tenantData.status}`">
|
||||
{{ getTenantStatusText(tenantData.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
@ -40,19 +31,28 @@
|
||||
<span>{{ formatCapacity(tenantData.capacity) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="已使用">
|
||||
<span>{{ formatCapacity(tenantData.capacity_used || 0) }}</span>
|
||||
<el-progress
|
||||
:percentage="getCapacityPercentage(tenantData.capacity, tenantData.capacity_used)"
|
||||
:color="getCapacityColor(tenantData.capacity, tenantData.capacity_used)"
|
||||
:stroke-width="6"
|
||||
style="margin-top: 8px; width: 200px;"
|
||||
/>
|
||||
<div style="display: flex;flex-direction: column;">
|
||||
<span>{{ formatCapacity(tenantData.capacity_used || 0) }}</span>
|
||||
<el-progress :percentage="getCapacityPercentage(tenantData.capacity, tenantData.capacity_used)"
|
||||
:color="getCapacityColor(tenantData.capacity, tenantData.capacity_used)" :stroke-width="6"
|
||||
style="margin-top: 8px;" />
|
||||
</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 label="创建时间">
|
||||
{{ tenantData.created_at }}
|
||||
{{ formatTime(tenantData.created_at) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ tenantData.updated_at || '未更新' }}
|
||||
{{ formatTime(tenantData.updated_at) || '未更新' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ tenantData.remark || '无' }}
|
||||
@ -69,7 +69,9 @@
|
||||
import { ref, watch, computed, onMounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getTenantDetail } from '@/api/tenant';
|
||||
import request from '@/utils/request';
|
||||
import { useDictStore } from '@/stores/dict';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
@ -125,7 +127,12 @@ function getAuditStatusType(status: string): 'warning' | 'success' | 'danger' |
|
||||
if (dictItem) {
|
||||
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'> = {
|
||||
pending: 'warning',
|
||||
@ -143,7 +150,7 @@ function getAuditStatusText(status: string) {
|
||||
if (dictItem) {
|
||||
return dictItem.dict_label || String(status);
|
||||
}
|
||||
|
||||
|
||||
// 兜底方案,保持原有逻辑
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: '待审核',
|
||||
@ -162,7 +169,7 @@ function getTenantStatusType(status: string | number): 'success' | 'info' | 'war
|
||||
if (dictItem) {
|
||||
return dictItem.dict_tag_type || 'info';
|
||||
}
|
||||
|
||||
|
||||
// 兜底方案,保持原有逻辑
|
||||
return String(status) === '1' ? 'success' : 'info';
|
||||
}
|
||||
@ -175,7 +182,7 @@ function getTenantStatusText(status: string | number): string {
|
||||
if (dictItem) {
|
||||
return dictItem.dict_label || String(status);
|
||||
}
|
||||
|
||||
|
||||
// 兜底方案,保持原有逻辑
|
||||
return String(status) === '1' ? '启用' : '禁用';
|
||||
}
|
||||
@ -213,10 +220,22 @@ function getCapacityColor(capacity: number | undefined, used: number | undefined
|
||||
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() {
|
||||
if (!props.tenantId) return;
|
||||
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getTenantDetail(props.tenantId);
|
||||
@ -277,6 +296,11 @@ watch(
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__cell) {
|
||||
width: 180px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions-item__label) {
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-regular);
|
||||
@ -286,5 +310,23 @@ watch(
|
||||
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-input v-model="tenantForm.name" placeholder="请输入租户名称" />
|
||||
</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-input v-model="tenantForm.owner" placeholder="请输入负责人" />
|
||||
</el-form-item>
|
||||
@ -56,6 +53,24 @@
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</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>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
@ -73,8 +88,14 @@ import {
|
||||
ElMessageBox,
|
||||
type FormInstance,
|
||||
type FormRules,
|
||||
type UploadFile,
|
||||
type UploadUserFile,
|
||||
type UploadRequestOptions,
|
||||
} from 'element-plus';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { createTenant, updateTenant } from '@/api/tenant';
|
||||
import { uploadFile } from '@/api/file';
|
||||
import request from '@/utils/request';
|
||||
import { useDictStore } from '@/stores/dict';
|
||||
|
||||
interface Props {
|
||||
@ -99,6 +120,9 @@ const visible = computed({
|
||||
|
||||
const submitting = ref(false);
|
||||
const tenantFormRef = ref<FormInstance>();
|
||||
const uploadFileList = ref<UploadUserFile[]>([]);
|
||||
const previewVisible = ref(false);
|
||||
const previewUrl = ref('');
|
||||
|
||||
// 字典数据
|
||||
const dictStore = useDictStore();
|
||||
@ -122,18 +146,17 @@ const isEditing = computed(() => {
|
||||
const tenantForm = reactive({
|
||||
id: null as number | null,
|
||||
name: '',
|
||||
code: '',
|
||||
owner: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
status: '1', // 改为字符串类型,与字典数据中的dict_value保持一致
|
||||
status: '1', // 改为字符串类型,与字典数据中的dict_value保持一致
|
||||
capacity: 0,
|
||||
remark: '',
|
||||
attachment_url: '' as string,
|
||||
});
|
||||
|
||||
const formRules: FormRules = {
|
||||
name: [{ required: true, message: '请输入租户名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入租户编码', trigger: 'blur' }],
|
||||
owner: [{ required: true, message: '请输入负责人', trigger: 'blur' }],
|
||||
phone: [
|
||||
{ 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() {
|
||||
tenantForm.id = null;
|
||||
tenantForm.name = '';
|
||||
tenantForm.code = '';
|
||||
tenantForm.owner = '';
|
||||
tenantForm.phone = '';
|
||||
tenantForm.email = '';
|
||||
tenantForm.status = '1'; // 改为字符串类型,与字典数据中的dict_value保持一致
|
||||
tenantForm.status = '1'; // 改为字符串类型,与字典数据中的dict_value保持一致
|
||||
tenantForm.capacity = 0;
|
||||
tenantForm.remark = '';
|
||||
tenantForm.attachment_url = '';
|
||||
uploadFileList.value = [];
|
||||
tenantFormRef.value?.resetFields();
|
||||
}
|
||||
|
||||
@ -165,7 +201,6 @@ function initFormData() {
|
||||
if (props.tenant && props.tenant.id) {
|
||||
tenantForm.id = props.tenant.id;
|
||||
tenantForm.name = props.tenant.name || '';
|
||||
tenantForm.code = props.tenant.code || '';
|
||||
tenantForm.owner = props.tenant.owner || '';
|
||||
tenantForm.phone = props.tenant.phone || '';
|
||||
tenantForm.email = props.tenant.email || '';
|
||||
@ -174,6 +209,10 @@ function initFormData() {
|
||||
// capacity 后端返回的是 MB,直接使用
|
||||
tenantForm.capacity = props.tenant.capacity != null ? Number(props.tenant.capacity) : 0;
|
||||
tenantForm.remark = props.tenant.remark || '';
|
||||
tenantForm.attachment_url = props.tenant.attachment_url || '';
|
||||
uploadFileList.value = tenantForm.attachment_url
|
||||
? [{ name: '附件', url: resolveFileUrl(tenantForm.attachment_url) }]
|
||||
: [];
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
@ -188,14 +227,14 @@ async function handleSubmit() {
|
||||
try {
|
||||
const tenantData = {
|
||||
name: tenantForm.name,
|
||||
code: tenantForm.code,
|
||||
owner: tenantForm.owner,
|
||||
phone: tenantForm.phone || '',
|
||||
email: tenantForm.email || '',
|
||||
status: Number(tenantForm.status), // 提交时转换为数字类型
|
||||
status: Number(tenantForm.status), // 提交时转换为数字类型
|
||||
// capacity 前后端都使用 MB
|
||||
capacity: tenantForm.capacity || 0,
|
||||
remark: tenantForm.remark || '',
|
||||
attachment_url: tenantForm.attachment_url || '',
|
||||
};
|
||||
|
||||
let res;
|
||||
@ -253,6 +292,55 @@ watch(
|
||||
},
|
||||
{ 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>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@ -262,4 +350,3 @@ watch(
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -4,11 +4,15 @@
|
||||
<h2 class="page-title">租户管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加租户
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
@ -32,17 +36,28 @@
|
||||
<!-- 租户列表 -->
|
||||
<div v-else>
|
||||
<el-card shadow="never">
|
||||
<el-table
|
||||
:data="tenants"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
element-loading-text="正在加载..."
|
||||
>
|
||||
<el-table :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="name" label="租户名称" min-width="120" />
|
||||
<el-table-column prop="code" label="租户编码" width="120" />
|
||||
<el-table-column prop="name" label="租户名称" min-width="240" />
|
||||
<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">
|
||||
<template #default="{ row }">
|
||||
<div class="capacity-cell">
|
||||
@ -51,60 +66,41 @@
|
||||
<span class="capacity-divider">/</span>
|
||||
<span>{{ formatCapacity(row.capacity ?? 0) }}</span>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="getCapacityPercentage(row.capacity, row.capacity_used)"
|
||||
:color="getCapacityColor(row.capacity, row.capacity_used)"
|
||||
:stroke-width="4"
|
||||
style="margin-top: 4px;"
|
||||
/>
|
||||
<el-progress :percentage="getCapacityPercentage(row.capacity, row.capacity_used)"
|
||||
:color="getCapacityColor(row.capacity, row.capacity_used)" :stroke-width="4"
|
||||
style="margin-top: 4px;" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="150" />
|
||||
<el-table-column label="审核状态" width="100">
|
||||
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getAuditStatusType(row.audit_status)">
|
||||
{{ 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>
|
||||
{{ formatTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>
|
||||
<el-icon>
|
||||
<View />
|
||||
</el-icon>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="success"
|
||||
v-if="row.audit_status === 'pending'"
|
||||
@click="handleAudit(row)"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
<el-button size="small" type="success" v-if="row.audit_status === 'pending'" @click="handleAudit(row)">
|
||||
<el-icon>
|
||||
<Check />
|
||||
</el-icon>
|
||||
审核
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
v-if="row.audit_status !== 'approved'"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
<el-button size="small" @click="handleEdit(row)" v-if="row.audit_status !== 'approved'">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
v-if="row.audit_status !== 'approved'"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)" v-if="row.audit_status !== 'approved'">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
@ -113,36 +109,19 @@
|
||||
</el-card>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
layout="total, prev, pager, next"
|
||||
/>
|
||||
<el-pagination background :current-page="page" :page-size="pageSize" :total="total"
|
||||
@current-change="handlePageChange" layout="total, prev, pager, next" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情组件 -->
|
||||
<TenantDetail
|
||||
v-model="showViewDialog"
|
||||
:tenant-id="currentTenant?.id"
|
||||
/>
|
||||
<TenantDetail v-model="showViewDialog" :tenant-id="currentTenant?.id" />
|
||||
|
||||
<!-- 审核组件 -->
|
||||
<TenantAudit
|
||||
v-model="showAuditDialog"
|
||||
:tenant="currentTenant"
|
||||
@success="handleAuditSuccess"
|
||||
/>
|
||||
<TenantAudit v-model="showAuditDialog" :tenant="currentTenant" @success="handleAuditSuccess" />
|
||||
|
||||
<!-- 编辑组件 -->
|
||||
<TenantEdit
|
||||
v-model="showEditDialog"
|
||||
:tenant="currentTenant"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
<TenantEdit v-model="showEditDialog" :tenant="currentTenant" @success="handleEditSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -155,6 +134,7 @@ import { useDictStore } from '@/stores/dict';
|
||||
import TenantDetail from './components/detail.vue';
|
||||
import TenantAudit from './components/audit.vue';
|
||||
import TenantEdit from './components/edit.vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 字典数据
|
||||
const reviewStatusDict = ref<any[]>([]);
|
||||
@ -229,6 +209,12 @@ const handlePageChange = (val: number) => {
|
||||
fetchTenants();
|
||||
};
|
||||
|
||||
//格式化时间
|
||||
function formatTime(time: string | null | undefined) {
|
||||
if (!time) return '';
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
// 刷新界面
|
||||
async function refresh() {
|
||||
try {
|
||||
@ -250,14 +236,14 @@ function getAuditStatusType(
|
||||
if (dictItem) {
|
||||
return dictItem.dict_tag_type || 'info';
|
||||
}
|
||||
|
||||
|
||||
// 兜底方案,保持原有逻辑
|
||||
const statusMap: Record<string, 'warning' | 'success' | 'danger' | 'info'> =
|
||||
{
|
||||
pending: 'warning',
|
||||
approved: 'success',
|
||||
rejected: 'danger',
|
||||
};
|
||||
{
|
||||
pending: 'warning',
|
||||
approved: 'success',
|
||||
rejected: 'danger',
|
||||
};
|
||||
return statusMap[status] || 'info';
|
||||
}
|
||||
|
||||
@ -269,7 +255,7 @@ function getAuditStatusText(status: string) {
|
||||
if (dictItem) {
|
||||
return dictItem.dict_label || String(status);
|
||||
}
|
||||
|
||||
|
||||
// 兜底方案,保持原有逻辑
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: '待审核',
|
||||
@ -288,7 +274,7 @@ function getTenantStatusType(status: string | number): 'success' | 'info' | 'war
|
||||
if (dictItem) {
|
||||
return dictItem.dict_tag_type || 'info';
|
||||
}
|
||||
|
||||
|
||||
// 兜底方案,保持原有逻辑
|
||||
return String(status) === '1' ? 'success' : 'info';
|
||||
}
|
||||
@ -301,7 +287,7 @@ function getTenantStatusText(status: string | number): string {
|
||||
if (dictItem) {
|
||||
return dictItem.dict_label || String(status);
|
||||
}
|
||||
|
||||
|
||||
// 兜底方案,保持原有逻辑
|
||||
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()
|
||||
}
|
||||
|
||||
// 仪表盘获取待办任务
|
||||
// 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()
|
||||
}
|
||||
|
||||
@ -48,6 +48,10 @@ func (f *FileInfo) TableName() string {
|
||||
return "yz_files"
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Removed duplicate ORM registration
|
||||
}
|
||||
|
||||
// CanPreview 判断文件是否可以在线预览
|
||||
func (f *FileInfo) CanPreview() bool {
|
||||
previewableExts := map[string]bool{
|
||||
|
||||
@ -121,3 +121,18 @@ func UpdateTask(t *Task, cols ...string) error {
|
||||
_, err := o.Update(t, cols...)
|
||||
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 {
|
||||
Id int `orm:"pk;auto" json:"id"`
|
||||
Name string `orm:"size(100)" json:"name"`
|
||||
Code string `orm:"size(50);unique" json:"code"`
|
||||
Owner string `orm:"size(50)" json:"owner"`
|
||||
Phone string `orm:"size(20);null" json:"phone"`
|
||||
Email string `orm:"size(100);null" json:"email"`
|
||||
Status int `orm:"default(1)" json:"status"`
|
||||
AuditStatus string `orm:"size(20);default(pending)" json:"audit_status"`
|
||||
AuditComment string `orm:"type(text);null" json:"audit_comment"`
|
||||
AuditBy string `orm:"size(50);null" json:"audit_by"`
|
||||
AuditTime *time.Time `orm:"null;type(datetime)" json:"audit_time"`
|
||||
Capacity int `orm:"default(0)" json:"capacity"`
|
||||
CapacityUsed int `orm:"default(0)" json:"capacity_used"`
|
||||
Remark string `orm:"type(text);null" json:"remark"`
|
||||
CreateTime time.Time `orm:"auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"null;type(datetime)" json:"delete_time"`
|
||||
CreateBy string `orm:"size(50);null" json:"create_by"`
|
||||
UpdateBy string `orm:"size(50);null" json:"update_by"`
|
||||
Id int `orm:"pk;auto" json:"id"`
|
||||
Name string `orm:"size(100)" json:"name"`
|
||||
Code string `orm:"size(50);unique" json:"code"`
|
||||
Owner string `orm:"size(50)" json:"owner"`
|
||||
Phone string `orm:"size(20);null" json:"phone"`
|
||||
Email string `orm:"size(100);null" json:"email"`
|
||||
Status int `orm:"default(1)" json:"status"`
|
||||
AuditStatus string `orm:"size(20);default(pending)" json:"audit_status"`
|
||||
AuditComment string `orm:"type(text);null" json:"audit_comment"`
|
||||
AuditBy string `orm:"size(50);null" json:"audit_by"`
|
||||
AuditTime *time.Time `orm:"null;type(datetime)" json:"audit_time"`
|
||||
Capacity int `orm:"default(0)" json:"capacity"`
|
||||
CapacityUsed int `orm:"default(0)" json:"capacity_used"`
|
||||
AttachmentUrl string `orm:"size(255);null" json:"attachment_url"`
|
||||
Remark string `orm:"type(text);null" json:"remark"`
|
||||
CreateTime time.Time `orm:"auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"null;type(datetime)" json:"delete_time"`
|
||||
CreateBy string `orm:"size(50);null" json:"create_by"`
|
||||
UpdateBy string `orm:"size(50);null" json:"update_by"`
|
||||
}
|
||||
|
||||
// TableName 设置表名
|
||||
|
||||
@ -270,6 +270,8 @@ func init() {
|
||||
// 文件管理路由 - 手动配置以匹配前端的 /api/files 路径
|
||||
beego.Router("/api/files", &controllers.FileController{}, "get:GetAllFiles")
|
||||
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/download/:id", &controllers.FileController{}, "get:DownloadFile")
|
||||
beego.Router("/api/files/preview/:id", &controllers.FileController{}, "get:PreviewFile")
|
||||
@ -315,6 +317,7 @@ func init() {
|
||||
// OA任务管理路由
|
||||
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/todo", &controllers.TaskController{}, "get:GetTodoTasks")
|
||||
|
||||
// 权限管理路由
|
||||
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 {
|
||||
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