增加租户储存容量
This commit is contained in:
parent
01c47ccbd4
commit
494e0160b7
@ -362,7 +362,7 @@ const findMenuItemByPath = (menus, path) => {
|
||||
}
|
||||
|
||||
h3{
|
||||
line-height: 60px;
|
||||
line-height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@ -65,28 +65,22 @@ export async function loadAndAddDynamicRoutes() {
|
||||
// 创建加载 Promise
|
||||
routesLoadingPromise = (async () => {
|
||||
try {
|
||||
console.log('[路由加载] 开始从 API 加载动态路由...');
|
||||
|
||||
// 直接从 API 获取菜单数据
|
||||
const { getAllMenus } = await import("@/api/menu");
|
||||
const res = await getAllMenus();
|
||||
|
||||
if (res && res.success && res.data) {
|
||||
console.log('[路由加载] API 返回菜单数量:', res.data.length);
|
||||
// 添加动态路由
|
||||
addDynamicRoutes(res.data);
|
||||
console.log('[路由加载] 动态路由加载完成');
|
||||
dynamicRoutesAdded = true;
|
||||
routesLoadingPromise = null;
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
console.warn('[路由加载] API 返回数据格式异常:', res);
|
||||
dynamicRoutesAdded = true;
|
||||
routesLoadingPromise = null;
|
||||
return Promise.resolve();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[路由加载] 加载动态路由失败:', error);
|
||||
// 即使出错也标记为已加载,避免无限重试
|
||||
dynamicRoutesAdded = true;
|
||||
routesLoadingPromise = null;
|
||||
@ -100,15 +94,11 @@ export async function loadAndAddDynamicRoutes() {
|
||||
// 添加动态路由到 Main 的 children 中
|
||||
function addDynamicRoutes(menus) {
|
||||
if (!menus?.length) {
|
||||
console.warn('[路由加载] 菜单数据为空,跳过添加动态路由');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[路由加载] 开始添加动态路由,菜单数量:', menus.length);
|
||||
|
||||
// 如果已经添加过,先移除旧路由(刷新时可能需要重新添加)
|
||||
if (dynamicRoutesAdded) {
|
||||
console.log('[路由加载] 检测到已添加的路由,先移除旧路由');
|
||||
// 移除 Main 路由以便重新添加
|
||||
if (router.hasRoute('Main')) {
|
||||
router.removeRoute('Main');
|
||||
@ -120,18 +110,10 @@ function addDynamicRoutes(menus) {
|
||||
const filteredMenus = menus.filter(menu => menu.id !== 1);
|
||||
|
||||
const dynamicRoutes = convertMenusToRoutes(filteredMenus);
|
||||
console.log('[路由加载] 转换后的动态路由数量:', dynamicRoutes.length);
|
||||
console.log('[路由加载] 动态路由列表:', dynamicRoutes.map(r => ({
|
||||
path: r.path,
|
||||
name: r.name,
|
||||
hasComponent: !!r.component,
|
||||
childrenCount: r.children?.length || 0
|
||||
})));
|
||||
|
||||
// 获取主路由
|
||||
const mainRoute = router.getRoutes().find(r => r.name === 'Main');
|
||||
if (!mainRoute) {
|
||||
console.error('[路由加载] 找不到 Main 路由');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -157,7 +139,6 @@ function addDynamicRoutes(menus) {
|
||||
};
|
||||
|
||||
router.addRoute(newMainRoute);
|
||||
console.log('[路由加载] Main 路由已更新,子路由总数:', newMainRoute.children.length);
|
||||
|
||||
// 添加知识库的子路由(详情页和编辑页)
|
||||
// 直接在 Main 路由下添加完整路径的子路由
|
||||
@ -227,22 +208,16 @@ router.beforeEach(async (to, from, next) => {
|
||||
|
||||
// 3. 已登录,确保动态路由已加载(必须在检查 404 之前)
|
||||
if (!dynamicRoutesAdded) {
|
||||
console.log('[路由守卫] 开始加载动态路由, 目标路径:', to.path);
|
||||
await loadAndAddDynamicRoutes();
|
||||
console.log('[路由守卫] 动态路由加载完成, dynamicRoutesAdded:', dynamicRoutesAdded);
|
||||
|
||||
// 如果路由加载后仍然未添加(API 失败)
|
||||
if (!dynamicRoutesAdded) {
|
||||
console.warn('[路由守卫] 路由加载失败,可能是 API 错误或 token 无效');
|
||||
// 重新检查 token,如果 token 不存在或无效,跳转到登录页
|
||||
const currentToken = localStorage.getItem('token');
|
||||
if (!currentToken) {
|
||||
console.log('[路由守卫] 未找到 token,跳转到登录页');
|
||||
next({ path: "/login", query: { redirect: to.path } });
|
||||
return;
|
||||
}
|
||||
// 如果 token 存在但路由加载失败,可能是 token 过期或无效,清除 token 并跳转登录
|
||||
console.warn('[路由守卫] Token 存在但路由加载失败,清除 token 并跳转登录页');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('userInfo');
|
||||
next({ path: "/login", query: { redirect: to.path } });
|
||||
@ -254,23 +229,14 @@ router.beforeEach(async (to, from, next) => {
|
||||
|
||||
// 重新解析路径检查是否能匹配
|
||||
const resolved = router.resolve(to.path);
|
||||
console.log('[路由守卫] 路由解析结果:', {
|
||||
path: to.path,
|
||||
resolved: resolved.path,
|
||||
matched: resolved.matched.length,
|
||||
name: resolved.name,
|
||||
matchedRoutes: resolved.matched.map(m => ({ name: m.name, path: m.path }))
|
||||
});
|
||||
|
||||
// 如果能匹配到路由,使用完整路径重新导航(确保Vue Router正确更新)
|
||||
if (resolved.matched.length > 0 && resolved.name !== "NotFound") {
|
||||
console.log('[路由守卫] 路由匹配成功,重新导航确保正确加载');
|
||||
next({ path: to.fullPath || to.path, replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果无法匹配,使用原始路径重新导航
|
||||
console.log('[路由守卫] 路由未匹配,重新导航到:', to.path);
|
||||
next({ path: to.fullPath || to.path, replace: true });
|
||||
return;
|
||||
}
|
||||
@ -278,29 +244,13 @@ router.beforeEach(async (to, from, next) => {
|
||||
// 3.5 如果路由已加载但当前路径未匹配,可能是刷新问题
|
||||
// 注意:这里需要排除根路径和已知的静态路由
|
||||
if (to.matched.length === 0 && to.name !== "NotFound" && to.path !== "/" && to.path !== "/dashboard") {
|
||||
console.log('[路由守卫] 路由已加载但未匹配,尝试重新解析:', to.path);
|
||||
// 重新解析路径,看看是否能够匹配
|
||||
const resolved = router.resolve(to.path);
|
||||
console.log('[路由守卫] 重新解析结果:', {
|
||||
matched: resolved.matched.length,
|
||||
name: resolved.name,
|
||||
matchedRoutes: resolved.matched.map(m => ({ name: m.name, path: m.path }))
|
||||
});
|
||||
|
||||
if (resolved.matched.length > 0 && resolved.name !== "NotFound") {
|
||||
// 能够匹配,说明路由表正常,使用路径重新导航
|
||||
console.log('[路由守卫] 重新解析匹配成功,重新导航');
|
||||
next({ path: to.path, replace: true });
|
||||
return;
|
||||
} else {
|
||||
console.warn('[路由守卫] 路由确实无法匹配,可能是路径错误:', to.path);
|
||||
// 打印所有已注册的路由用于调试
|
||||
const allRoutes = router.getRoutes();
|
||||
console.log('[路由守卫] 所有已注册的路由:', allRoutes.map(r => ({
|
||||
name: r.name,
|
||||
path: r.path,
|
||||
children: r.children?.map(c => ({ name: c.name, path: c.path }))
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import CommonHeader from '@/components/CommonHeader.vue';
|
||||
import { useTabsStore } from '@/stores';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { ref, watch, reactive, nextTick, onMounted } from 'vue';
|
||||
import { More, DArrowRight } from '@element-plus/icons-vue'
|
||||
import { More, Close, CircleClose } from '@element-plus/icons-vue'
|
||||
|
||||
const tabsStore = useTabsStore();
|
||||
const router = useRouter();
|
||||
@ -175,16 +175,20 @@ function closeAllTabs() {
|
||||
@contextmenu="onTabContextMenu($event, tab)"
|
||||
/>
|
||||
</el-tabs>
|
||||
<!-- 跟随浮动到 tabs 最右侧的批量按钮 -->
|
||||
<div class="floated-tabs-extra-btn">
|
||||
<el-dropdown>
|
||||
<span class="extra-action-btn">
|
||||
<el-icon><DArrowRight /></el-icon>
|
||||
</span>
|
||||
<!-- 右侧操作按钮 -->
|
||||
<div class="tabs-extra-actions">
|
||||
<el-dropdown trigger="click">
|
||||
<el-button type="primary" link size="small" class="extra-btn">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="closeOthers">关闭其他</el-dropdown-item>
|
||||
<el-dropdown-item @click="closeAll">关闭全部</el-dropdown-item>
|
||||
<el-dropdown-item @click="closeOthers">
|
||||
关闭其他
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="closeAll">
|
||||
关闭全部
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@ -220,47 +224,132 @@ function closeAllTabs() {
|
||||
.main-header {
|
||||
background-color: var(--header-bg-color, #0081ff);
|
||||
transition: background-color 0.3s ease;
|
||||
height: 60px;
|
||||
height: 80px;
|
||||
padding: 0;
|
||||
}
|
||||
.right-main {
|
||||
background-color: var(--bg-color-page);
|
||||
color: var(--text-color-primary);
|
||||
background-color: var(--el-bg-color-page);
|
||||
color: var(--el-text-color-primary);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
// 注意:overflow-y: auto 不会影响 fixed 定位的元素(fixed 相对于 viewport)
|
||||
|
||||
.multi-tabs-wrapper {
|
||||
position: relative;
|
||||
zoom: 1;
|
||||
min-height: 45px;
|
||||
}
|
||||
.floated-tabs-extra-btn {
|
||||
float: right;
|
||||
margin-top: -40px;
|
||||
margin-right: 12px;
|
||||
z-index: 10;
|
||||
}
|
||||
.extra-action-btn {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: #888;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
transition: color 0.2s;
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
background: none;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.multi-tabs {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap) {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--el-border-color) transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--el-border-color);
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-border-color-darker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
margin-right: 8px;
|
||||
padding: 8px 16px;
|
||||
height: 36px;
|
||||
line-height: 20px;
|
||||
color: var(--el-text-color-regular);
|
||||
background: var(--el-fill-color-lighter);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary-light-7);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--el-color-primary);
|
||||
border-color: #4f84ff;
|
||||
background: #4f84ff;
|
||||
color: #fff;
|
||||
|
||||
.el-icon-close {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-icon-close {
|
||||
margin-left: 8px;
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.more-menu {
|
||||
font-size: 13px;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
.tabs-extra-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.extra-btn {
|
||||
padding: 8px;
|
||||
font-size: 18px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,173 @@
|
||||
<template>
|
||||
<div>
|
||||
123
|
||||
<div class="category-manage">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">分类管理</h2>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加分类
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="category-content">
|
||||
<el-card shadow="never">
|
||||
<el-table :data="categoryList" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="categoryId" label="ID" width="80" />
|
||||
<el-table-column prop="categoryName" label="分类名称" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
ref="formRef"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="分类名称" prop="categoryName">
|
||||
<el-input
|
||||
v-model="formData.categoryName"
|
||||
placeholder="请输入分类名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { getCategoryList, addCategory } from '@/api/knowledge'
|
||||
|
||||
const loading = ref(false)
|
||||
const categoryList = ref<any[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加分类')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const formData = reactive({
|
||||
categoryId: 0,
|
||||
categoryName: '',
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
categoryName: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
],
|
||||
})
|
||||
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getCategoryList()
|
||||
const data = res.code === 0 && res.data ? res.data : res.data || []
|
||||
categoryList.value = Array.isArray(data) ? data : []
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '获取分类列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '添加分类'
|
||||
formData.categoryId = 0
|
||||
formData.categoryName = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
dialogTitle.value = '编辑分类'
|
||||
formData.categoryId = row.categoryId
|
||||
formData.categoryName = row.categoryName
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = (row: any) => {
|
||||
ElMessageBox.confirm('确认删除该分类?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
try {
|
||||
// TODO: 实现删除API
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formRef.value) return
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
await addCategory({
|
||||
categoryName: formData.categoryName,
|
||||
})
|
||||
ElMessage.success('操作成功')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped lang="less">
|
||||
.category-manage {
|
||||
padding: 24px;
|
||||
min-height: 100%;
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
</style>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.category-content {
|
||||
:deep(.el-card) {
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -2,68 +2,76 @@
|
||||
<div class="knowledge-detail">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="detail-header">
|
||||
<el-button type="text" @click="goBack"
|
||||
><i class="fas fa-arrow-left"></i> 返回</el-button
|
||||
>
|
||||
<h2>{{ knowledgeTitle }}</h2>
|
||||
<el-button type="primary" link @click="goBack">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
<h2 class="detail-title">{{ knowledgeTitle }}</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleEdit">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleDelete">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容区 -->
|
||||
<div class="detail-body">
|
||||
<!-- 左侧信息面板 -->
|
||||
<div class="info-panel">
|
||||
<el-card shadow="never">
|
||||
<div slot="header" class="header">
|
||||
<span>基本信息</span>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3 class="info-title">基本信息</h3>
|
||||
<el-divider />
|
||||
<el-form label-width="80px" size="small">
|
||||
<el-form-item label="标题:">
|
||||
<span>{{ formData.title }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类:">
|
||||
<el-tag size="small">{{ formData.category }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签:">
|
||||
<el-tag
|
||||
v-for="tag in formData.tags"
|
||||
:key="tag"
|
||||
size="small"
|
||||
style="margin-right: 4px"
|
||||
>{{ tag }}</el-tag
|
||||
>
|
||||
</el-form-item>
|
||||
<el-form-item label="作者:">
|
||||
<span>{{ formData.author }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间:">
|
||||
<span>{{ formData.createTime }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="更新时间:">
|
||||
<span>{{ formData.updateTime }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-form label-width="80px" align="center">
|
||||
<el-divider />
|
||||
<el-button type="primary" @click="handleEdit"
|
||||
><i class="fas fa-edit"></i> 编辑</el-button
|
||||
>
|
||||
<el-button type="danger" @click="handleDelete"
|
||||
><i class="fas fa-trash"></i> 删除</el-button
|
||||
>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<div class="info-content">
|
||||
<div class="info-item">
|
||||
<span class="info-label">标题:</span>
|
||||
<span class="info-value">{{ formData.title }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">分类:</span>
|
||||
<el-tag size="small" type="info">{{ formData.category }}</el-tag>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">标签:</span>
|
||||
<div class="tags-wrapper">
|
||||
<el-tag
|
||||
v-for="tag in formData.tags"
|
||||
:key="tag"
|
||||
size="small"
|
||||
effect="plain"
|
||||
style="margin-right: 6px; margin-bottom: 6px;"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">作者:</span>
|
||||
<span class="info-value">{{ formData.author }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">创建时间:</span>
|
||||
<span class="info-value">{{ formData.createTime }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">更新时间:</span>
|
||||
<span class="info-value">{{ formData.updateTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容面板 -->
|
||||
<div class="content-panel">
|
||||
<el-card shadow="never">
|
||||
<div slot="header">
|
||||
<span>正文内容</span>
|
||||
</div>
|
||||
<div class="content-card">
|
||||
<h3 class="content-title">正文内容</h3>
|
||||
<el-divider />
|
||||
<div class="markdown-body" v-html="compiledMarkdown"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -73,6 +81,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import { marked } from "marked"
|
||||
import { getKnowledgeDetail, deleteKnowledge } from "@/api/knowledge"
|
||||
|
||||
@ -90,6 +99,7 @@ interface FormData {
|
||||
updateTime: string,
|
||||
content: string,
|
||||
}
|
||||
|
||||
const formData = ref<FormData>({
|
||||
title: "",
|
||||
category: "",
|
||||
@ -124,7 +134,6 @@ async function fetchDetail() {
|
||||
try {
|
||||
const idValue = id.value as string | number
|
||||
const res = await getKnowledgeDetail(idValue)
|
||||
// API 返回的数据结构: { code: 0, data: {...}, message: "success" }
|
||||
const data = (res.code === 0 && res.data) ? res.data : (res.data || res)
|
||||
formData.value = {
|
||||
title: data.title || '',
|
||||
@ -142,14 +151,17 @@ async function fetchDetail() {
|
||||
ElMessage.error("获取详情失败")
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchDetail)
|
||||
|
||||
function goBack() {
|
||||
router.push("/apps/knowledge")
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
router.push(`/apps/knowledge/edit/${id.value}`)
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
ElMessageBox.confirm(
|
||||
"确认删除该知识?",
|
||||
@ -163,7 +175,6 @@ function handleDelete() {
|
||||
try {
|
||||
const idValue = id.value as string | number
|
||||
const res = await deleteKnowledge(idValue)
|
||||
// API 返回的数据结构: { code: 0, message: "删除成功", data: null }
|
||||
if (res.code === 0) {
|
||||
ElMessage.success("删除成功")
|
||||
goBack()
|
||||
@ -178,32 +189,35 @@ function handleDelete() {
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
/* 知识详情页面样式 - 使用主题变量 */
|
||||
.knowledge-detail {
|
||||
background-color: var(--bg-color-page);
|
||||
min-height: 100%;
|
||||
padding: 24px;
|
||||
transition: background-color 0.3s ease;
|
||||
min-height: 100%;
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
background-color: var(--card-bg-color);
|
||||
padding: 16px 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--box-shadow);
|
||||
border: 1px solid var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
gap: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.detail-header h2 {
|
||||
margin: 0 0 0 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color-primary);
|
||||
transition: color 0.3s ease;
|
||||
.detail-title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
@ -215,94 +229,120 @@ function handleDelete() {
|
||||
width: 320px;
|
||||
flex-shrink: 0;
|
||||
|
||||
:deep(.el-card) {
|
||||
background-color: var(--card-bg-color);
|
||||
border-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.info-card {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.header {
|
||||
color: var(--text-color-primary);
|
||||
font-weight: 600;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.info-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
color: var(--text-color-secondary);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.info-content {
|
||||
.info-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
color: var(--text-color-primary);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tag) {
|
||||
background-color: var(--fill-color-light);
|
||||
border-color: var(--border-color-lighter);
|
||||
color: var(--text-color-primary);
|
||||
transition: all 0.3s ease;
|
||||
.info-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tags-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
:deep(.el-card) {
|
||||
background-color: var(--card-bg-color);
|
||||
border-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.content-card {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
:deep(.el-card__header) {
|
||||
color: var(--text-color-primary);
|
||||
border-bottom-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
.content-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
color: var(--text-color-primary);
|
||||
transition: color 0.3s ease;
|
||||
color: var(--el-text-color-primary);
|
||||
min-height: 400px;
|
||||
|
||||
// Markdown 内容样式适配主题
|
||||
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
|
||||
color: var(--text-color-primary);
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(h1) { font-size: 28px; }
|
||||
:deep(h2) { font-size: 24px; }
|
||||
:deep(h3) { font-size: 20px; }
|
||||
|
||||
:deep(p), :deep(li), :deep(span), :deep(div) {
|
||||
color: var(--text-color-primary);
|
||||
margin: 8px 0;
|
||||
color: var(--el-text-color-primary);
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: var(--primary-color);
|
||||
color: #4f84ff;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(code) {
|
||||
background-color: var(--fill-color-light);
|
||||
color: var(--text-color-primary);
|
||||
border-color: var(--border-color-lighter);
|
||||
background-color: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-primary);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
background-color: var(--fill-color-light);
|
||||
border-color: var(--border-color-lighter);
|
||||
padding: 12px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 16px 0;
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
@ -314,21 +354,24 @@ function handleDelete() {
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
margin: 8px 0;
|
||||
margin: 16px 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:deep(ul), :deep(ol) {
|
||||
color: var(--text-color-primary);
|
||||
margin: 8px 0;
|
||||
color: var(--el-text-color-primary);
|
||||
margin: 12px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
:deep(blockquote) {
|
||||
border-left: 4px solid var(--primary-color);
|
||||
border-left: 4px solid #4f84ff;
|
||||
padding-left: 16px;
|
||||
margin: 16px 0;
|
||||
color: var(--text-color-secondary);
|
||||
background-color: var(--fill-color-extra-light);
|
||||
color: var(--el-text-color-regular);
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(table) {
|
||||
@ -337,28 +380,23 @@ function handleDelete() {
|
||||
margin: 16px 0;
|
||||
|
||||
th, td {
|
||||
border: 1px solid var(--border-color-lighter);
|
||||
padding: 8px 12px;
|
||||
color: var(--text-color-primary);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
padding: 12px;
|
||||
color: var(--el-text-color-primary);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--fill-color-light);
|
||||
background-color: var(--el-fill-color-light);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 响应式布局 */
|
||||
@media (max-width: 768px) {
|
||||
.detail-body {
|
||||
flex-direction: column;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
@ -366,11 +404,14 @@ function handleDelete() {
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
.detail-header h2 {
|
||||
font-size: 16px;
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -2,15 +2,18 @@
|
||||
<div class="knowledge-edit">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="edit-header">
|
||||
<el-button type="text" @click="goBack"
|
||||
><i class="fas fa-arrow-left"></i> 返回</el-button
|
||||
>
|
||||
<h2>{{ isEdit ? "编辑知识" : "新建知识" }}</h2>
|
||||
<el-button type="primary" link @click="goBack">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
<h2 class="edit-title">{{ isEdit ? "编辑知识" : "新建知识" }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<div class="edit-meta">
|
||||
<el-card shadow="never">
|
||||
<div class="meta-card">
|
||||
<h3 class="meta-title">基本信息</h3>
|
||||
<el-divider />
|
||||
<el-form
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
@ -18,16 +21,16 @@
|
||||
label-width="80px"
|
||||
size="default"
|
||||
>
|
||||
<el-row :gutter="20" style="margin-bottom: 20px;">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="标题:" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入标题" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入标题" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="分类:" prop="category">
|
||||
<el-form-item label="分类" prop="category">
|
||||
<el-select
|
||||
v-model="formData.category"
|
||||
placeholder="请选择分类"
|
||||
@ -44,7 +47,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="标签:" prop="tags">
|
||||
<el-form-item label="标签" prop="tags">
|
||||
<el-select
|
||||
v-model="formData.tags"
|
||||
multiple
|
||||
@ -63,12 +66,12 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="作者:" prop="author">
|
||||
<el-form-item label="作者" prop="author">
|
||||
<el-input v-model="formData.author" placeholder="请输入作者" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="权限:" prop="share">
|
||||
<el-form-item label="权限" prop="share">
|
||||
<el-radio-group v-model="formData.share">
|
||||
<el-radio-button :label="0">个人</el-radio-button>
|
||||
<el-radio-button :label="1">共享</el-radio-button>
|
||||
@ -77,35 +80,35 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<el-divider />
|
||||
<div class="meta-actions">
|
||||
<el-button type="primary" @click="handleSubmit"
|
||||
><i class="fas fa-save"></i> 保存</el-button
|
||||
>
|
||||
<el-button type="primary" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>
|
||||
保存
|
||||
</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 正文内容区 - 左右结构 -->
|
||||
<div class="edit-body">
|
||||
<!-- 左侧编辑器 -->
|
||||
<div class="editor-panel">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<span>编辑正文</span>
|
||||
</template>
|
||||
<WangEditor v-model="formData.content" />
|
||||
</el-card>
|
||||
<div class="editor-card">
|
||||
<h3 class="editor-title">编辑正文</h3>
|
||||
<el-divider />
|
||||
<div class="editor-content">
|
||||
<WangEditor v-model="formData.content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧预览 -->
|
||||
<div class="preview-panel">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<span>预览效果</span>
|
||||
</template>
|
||||
<div class="preview-card">
|
||||
<h3 class="preview-title">预览效果</h3>
|
||||
<el-divider />
|
||||
<div class="markdown-body" v-html="compiledMarkdown"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -115,18 +118,18 @@
|
||||
import { ref, reactive, computed, onMounted, watch } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { marked } from "marked";
|
||||
import { ArrowLeft, Check } from '@element-plus/icons-vue'
|
||||
// @ts-ignore
|
||||
import { getKnowledgeDetail, createKnowledge, updateKnowledge, getCategoryList, getTagList } from "@/api/knowledge"; // Changed to getKnowledgeDetail
|
||||
import { getKnowledgeDetail, createKnowledge, updateKnowledge, getCategoryList, getTagList } from "@/api/knowledge";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
import WangEditor from '@/views/components/WangEditor.vue';
|
||||
|
||||
// 表单 DOM 引用
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<{
|
||||
title: string;
|
||||
category: string;
|
||||
@ -145,7 +148,6 @@ const formData = reactive<{
|
||||
share: 0,
|
||||
});
|
||||
|
||||
// 校验规则
|
||||
const rules = reactive<FormRules>({
|
||||
title: [{ required: true, message: "请输入标题", trigger: "blur" }],
|
||||
category: [{ required: true, message: "请选择分类", trigger: "change" }],
|
||||
@ -153,7 +155,6 @@ const rules = reactive<FormRules>({
|
||||
content: [{ required: true, message: "请输入正文", trigger: "blur" }],
|
||||
});
|
||||
|
||||
// 分类与标签数据
|
||||
interface CategoryItem {
|
||||
categoryId: number;
|
||||
categoryName: string;
|
||||
@ -161,18 +162,14 @@ interface CategoryItem {
|
||||
const categoryList = ref<CategoryItem[]>([]);
|
||||
const tagList = ref<string[]>([]);
|
||||
|
||||
// id: 编辑时有,新增为 'new'
|
||||
const id = computed(() => route.params.id || route.query.id);
|
||||
const isEdit = computed(() => {
|
||||
const currentId = id.value;
|
||||
return !!currentId && currentId !== "new" && currentId !== "";
|
||||
});
|
||||
|
||||
// markdown 预览
|
||||
const compiledMarkdown = computed(() => marked(formData.content || ""));
|
||||
|
||||
// TODO: 后续可以集成 wangEditor
|
||||
// 暂时使用 textarea,监听内容变化
|
||||
watch(
|
||||
() => formData.content,
|
||||
() => {
|
||||
@ -180,7 +177,6 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
// 获取登录用户信息
|
||||
const getLoginUser = () => {
|
||||
const userStr = localStorage.getItem("user");
|
||||
if (userStr) {
|
||||
@ -194,25 +190,20 @@ const getLoginUser = () => {
|
||||
return "";
|
||||
};
|
||||
|
||||
// 获取详情
|
||||
const fetchDetail = async () => {
|
||||
try {
|
||||
const currentId = id.value as string;
|
||||
if (currentId && currentId !== "new") {
|
||||
const res = await getKnowledgeDetail(parseInt(currentId as string)); // Changed to getKnowledgeDetail
|
||||
// API 返回的数据结构: { code: 0, data: {...}, message: "success" }
|
||||
const res = await getKnowledgeDetail(parseInt(currentId as string));
|
||||
const data = res.code === 0 && res.data ? res.data : res.data || res;
|
||||
|
||||
// 映射后端数据到前端表单
|
||||
// categoryName 是从数据库联查得到的分类名称
|
||||
formData.title = data.title || "";
|
||||
formData.category = data.categoryName || "";
|
||||
formData.categoryId = data.categoryId || 0;
|
||||
formData.author = data.author || "";
|
||||
formData.content = data.content || "";
|
||||
formData.share = data.share || 0; // Added share loading
|
||||
formData.share = data.share || 0;
|
||||
|
||||
// 如果没有 categoryId,尝试根据 categoryName 查找
|
||||
if (!formData.categoryId && formData.category) {
|
||||
const foundCategory = categoryList.value.find(
|
||||
(item) => item.categoryName === formData.category
|
||||
@ -222,14 +213,12 @@ const fetchDetail = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Tags 可能是 JSON 字符串,需要解析
|
||||
if (data.tags) {
|
||||
try {
|
||||
const parsed =
|
||||
typeof data.tags === "string" ? JSON.parse(data.tags) : data.tags;
|
||||
formData.tags = Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
// 如果解析失败,尝试作为字符串数组处理
|
||||
formData.tags = Array.isArray(data.tags) ? data.tags : [];
|
||||
}
|
||||
} else {
|
||||
@ -241,7 +230,6 @@ const fetchDetail = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 获取分类和标签
|
||||
const loadCategoryAndTag = async () => {
|
||||
try {
|
||||
const [catRes, tagRes] = await Promise.all([
|
||||
@ -249,14 +237,12 @@ const loadCategoryAndTag = async () => {
|
||||
getTagList(),
|
||||
]);
|
||||
|
||||
// API 返回的数据结构: { code: 0, data: [...], message: "success" }
|
||||
const categories =
|
||||
catRes.code === 0 && catRes.data ? catRes.data : catRes.data || [];
|
||||
const tags =
|
||||
tagRes.code === 0 && tagRes.data ? tagRes.data : tagRes.data || [];
|
||||
|
||||
categoryList.value = Array.isArray(categories) ? categories : [];
|
||||
// 标签数据可能是 { tagId, tagName } 格式,需要提取 tagName
|
||||
tagList.value = Array.isArray(tags)
|
||||
? tags.map((tag) => tag.tagName || tag)
|
||||
: [];
|
||||
@ -267,7 +253,6 @@ const loadCategoryAndTag = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理分类变化
|
||||
const handleCategoryChange = (categoryName: string) => {
|
||||
const selectedCategory = categoryList.value.find(
|
||||
(item) => item.categoryName === categoryName
|
||||
@ -277,12 +262,10 @@ const handleCategoryChange = (categoryName: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 返回
|
||||
const goBack = () => {
|
||||
router.push("/apps/knowledge");
|
||||
};
|
||||
|
||||
// 保存
|
||||
const handleSubmit = () => {
|
||||
if (!formRef.value) return;
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
@ -290,14 +273,11 @@ const handleSubmit = () => {
|
||||
|
||||
const currentId = id.value as string;
|
||||
|
||||
// 检查 categoryId 是否已设置
|
||||
if (!formData.categoryId) {
|
||||
ElMessage.warning("请选择分类");
|
||||
return;
|
||||
}
|
||||
|
||||
// 准备提交的数据(符合后端格式,字段名对应后端 JSON tag)
|
||||
// 确保数据类型正确:categoryId 必须是数字
|
||||
const submitData: any = {
|
||||
title: formData.title || "",
|
||||
categoryId: Number(formData.categoryId) || 0,
|
||||
@ -307,21 +287,18 @@ const handleSubmit = () => {
|
||||
Array.isArray(formData.tags) && formData.tags.length > 0
|
||||
? JSON.stringify(formData.tags)
|
||||
: "[]",
|
||||
status: 1, // 默认已发布
|
||||
share: formData.share, // Added share in params
|
||||
status: 1,
|
||||
share: formData.share,
|
||||
};
|
||||
|
||||
if (isEdit.value && currentId !== "new") {
|
||||
// 编辑:需要添加 id(后端 JSON tag 是 "id")
|
||||
const knowledgeId = parseInt(currentId as string);
|
||||
if (isNaN(knowledgeId) || knowledgeId <= 0) {
|
||||
ElMessage.error("知识ID无效");
|
||||
return;
|
||||
}
|
||||
// updateKnowledge 函数会将 id 合并到 data 中,所以这里不需要手动添加
|
||||
try {
|
||||
const res = await updateKnowledge(knowledgeId, submitData);
|
||||
// API 返回的数据结构: { code: 0, message: "更新成功", data: null }
|
||||
if (res.code === 0) {
|
||||
ElMessage.success("保存成功");
|
||||
goBack();
|
||||
@ -335,10 +312,8 @@ const handleSubmit = () => {
|
||||
ElMessage.error(errorMessage);
|
||||
}
|
||||
} else {
|
||||
// 新建:不包含 id 字段
|
||||
try {
|
||||
const res = await createKnowledge(submitData);
|
||||
// API 返回的数据结构: { code: 0, message: "创建成功", data: { id: ... } }
|
||||
if (res.code === 0) {
|
||||
ElMessage.success("创建成功");
|
||||
goBack();
|
||||
@ -355,41 +330,17 @@ const handleSubmit = () => {
|
||||
});
|
||||
};
|
||||
|
||||
//删除功能
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
// Assuming deleteKnowledge is imported or defined elsewhere
|
||||
// await deleteKnowledge(id.value as string);
|
||||
ElMessage.success("删除成功");
|
||||
goBack();
|
||||
} catch (e: any) {
|
||||
console.error("删除失败:", e);
|
||||
}
|
||||
ElMessageBox.confirm("确认删除该知识?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
await handleDelete();
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
// 获取作者信息
|
||||
const author = getLoginUser();
|
||||
if (author && !isEdit.value) {
|
||||
formData.author = author;
|
||||
}
|
||||
|
||||
// 先加载分类和标签
|
||||
await loadCategoryAndTag();
|
||||
|
||||
// 如果是编辑模式,获取详情
|
||||
if (isEdit.value) {
|
||||
await fetchDetail();
|
||||
} else {
|
||||
// 如果是新建模式,设置默认分类
|
||||
if (categoryList.value.length > 0 && !formData.category) {
|
||||
formData.category = categoryList.value[0].categoryName;
|
||||
formData.categoryId = categoryList.value[0].categoryId;
|
||||
@ -397,7 +348,6 @@ onMounted(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// 暴露到模板
|
||||
defineExpose({
|
||||
formRef,
|
||||
});
|
||||
@ -405,165 +355,123 @@ defineExpose({
|
||||
|
||||
<style scoped lang="less">
|
||||
.knowledge-edit {
|
||||
background-color: var(--bg-color-page);
|
||||
min-height: 100%;
|
||||
padding: 24px;
|
||||
transition: background-color 0.3s ease;
|
||||
min-height: 100%;
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.edit-header {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
background-color: var(--card-bg-color);
|
||||
padding: 16px 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--box-shadow);
|
||||
border: 1px solid var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
gap: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.edit-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.edit-header h2 {
|
||||
margin: 0 0 0 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color-primary);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
/* 基本信息区 */
|
||||
.edit-meta {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.el-card) {
|
||||
background-color: var(--card-bg-color);
|
||||
border-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.meta-card {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
:deep(.el-card__header) {
|
||||
color: var(--text-color-primary);
|
||||
border-bottom-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.meta-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-divider) {
|
||||
border-color: var(--border-color-lighter);
|
||||
transition: border-color 0.3s ease;
|
||||
.meta-actions {
|
||||
margin-top: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.meta-actions {
|
||||
margin-top: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 正文编辑区 - 左右结构 */
|
||||
.edit-body {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.editor-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
:deep(.el-card) {
|
||||
background-color: var(--card-bg-color);
|
||||
border-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
color: var(--text-color-primary);
|
||||
border-bottom-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-panel,
|
||||
.preview-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.el-card) {
|
||||
background-color: var(--card-bg-color);
|
||||
border-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
.editor-card,
|
||||
.preview-card {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.editor-title,
|
||||
.preview-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
color: var(--text-color-primary);
|
||||
border-bottom-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
.editor-content {
|
||||
flex: 1;
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: var(--text-color-primary);
|
||||
min-height: 400px;
|
||||
padding: 14px 16px;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
:deep(h1),
|
||||
:deep(h2),
|
||||
:deep(h3),
|
||||
:deep(h4),
|
||||
:deep(h5),
|
||||
:deep(h6) {
|
||||
color: var(--text-color-primary);
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(p),
|
||||
:deep(li),
|
||||
:deep(span),
|
||||
:deep(div) {
|
||||
color: var(--text-color-primary);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(code) {
|
||||
background-color: var(--fill-color-light);
|
||||
color: var(--text-color-primary);
|
||||
border-color: var(--border-color-lighter);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: "Courier New", monospace;
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
background-color: var(--fill-color-light);
|
||||
border-color: var(--border-color-lighter);
|
||||
padding: 12px;
|
||||
.preview-card {
|
||||
.markdown-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: var(--el-text-color-primary);
|
||||
padding: 16px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
:deep(h1), :deep(h2), :deep(h3) {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
margin: 8px 0;
|
||||
:deep(p), :deep(li) {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
:deep(code) {
|
||||
background-color: var(--el-fill-color-light);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
background-color: var(--el-fill-color-light);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -578,33 +486,5 @@ defineExpose({
|
||||
.preview-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.edit-header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.edit-header h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
:deep(.el-form-item--default) {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-textarea__inner) {
|
||||
background-color: var(--fill-color-blank) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
&::placeholder {
|
||||
color: var(--text-color-placeholder) !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div class="tag-manage">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">标签管理</h2>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加标签
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="tag-content">
|
||||
<el-card shadow="never">
|
||||
<el-table :data="tagList" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="tagId" label="ID" width="80" />
|
||||
<el-table-column prop="tagName" label="标签名称" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
ref="formRef"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="标签名称" prop="tagName">
|
||||
<el-input
|
||||
v-model="formData.tagName"
|
||||
placeholder="请输入标签名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { getTagList, addTag } from '@/api/knowledge'
|
||||
|
||||
const loading = ref(false)
|
||||
const tagList = ref<any[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加标签')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const formData = reactive({
|
||||
tagId: 0,
|
||||
tagName: '',
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
tagName: [
|
||||
{ required: true, message: '请输入标签名称', trigger: 'blur' },
|
||||
],
|
||||
})
|
||||
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getTagList()
|
||||
const data = res.code === 0 && res.data ? res.data : res.data || []
|
||||
tagList.value = Array.isArray(data) ? data : []
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '获取标签列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '添加标签'
|
||||
formData.tagId = 0
|
||||
formData.tagName = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
dialogTitle.value = '编辑标签'
|
||||
formData.tagId = row.tagId
|
||||
formData.tagName = row.tagName
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = (row: any) => {
|
||||
ElMessageBox.confirm('确认删除该标签?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
try {
|
||||
// TODO: 实现删除API
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formRef.value) return
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
await addTag({
|
||||
tagName: formData.tagName,
|
||||
})
|
||||
ElMessage.success('操作成功')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tag-manage {
|
||||
padding: 24px;
|
||||
min-height: 100%;
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.tag-content {
|
||||
:deep(.el-card) {
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
title="租户审核"
|
||||
v-model="visible"
|
||||
width="700px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="audit-content" v-loading="loading">
|
||||
<!-- 租户信息展示 -->
|
||||
<div class="tenant-info-section">
|
||||
<h3 class="section-title">租户信息</h3>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="租户ID">
|
||||
{{ tenantData.id }}
|
||||
</el-descriptions-item>
|
||||
<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>
|
||||
<el-descriptions-item label="联系电话">
|
||||
{{ tenantData.phone || '未设置' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">
|
||||
{{ tenantData.email || '未设置' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="tenantData.status === 'enabled' ? 'success' : 'info'">
|
||||
{{ tenantData.status === 'enabled' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ tenantData.created_at }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ tenantData.remark || '无' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 审核表单 -->
|
||||
<div class="audit-form-section">
|
||||
<h3 class="section-title">审核信息</h3>
|
||||
<el-form
|
||||
:model="auditForm"
|
||||
:rules="auditRules"
|
||||
ref="auditFormRef"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="审核结果" prop="result">
|
||||
<el-radio-group v-model="auditForm.result">
|
||||
<el-radio value="approved">通过</el-radio>
|
||||
<el-radio value="rejected">拒绝</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核意见" prop="comment">
|
||||
<el-input
|
||||
v-model="auditForm.comment"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入审核意见"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
提交审核
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { auditTenant } from '@/api/tenant';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
tenant: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
tenant: () => ({}),
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const tenantData = ref<any>({});
|
||||
|
||||
// 审核表单
|
||||
const auditForm = reactive({
|
||||
result: '',
|
||||
comment: '',
|
||||
});
|
||||
|
||||
const auditFormRef = ref<FormInstance>();
|
||||
|
||||
const auditRules: FormRules = {
|
||||
result: [{ required: true, message: '请选择审核结果', trigger: 'change' }],
|
||||
comment: [{ required: true, message: '请输入审核意见', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
// 获取当前用户
|
||||
function getCurrentUser() {
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
return user.username || user.name || user.userName || 'system';
|
||||
} catch {
|
||||
return 'system';
|
||||
}
|
||||
}
|
||||
return 'system';
|
||||
}
|
||||
|
||||
// 提交审核
|
||||
async function handleSubmit() {
|
||||
if (!auditFormRef.value) return;
|
||||
|
||||
await auditFormRef.value.validate();
|
||||
submitting.value = true;
|
||||
try {
|
||||
const auditData = {
|
||||
audit_status: auditForm.result,
|
||||
audit_comment: auditForm.comment,
|
||||
audit_by: getCurrentUser(),
|
||||
};
|
||||
|
||||
const res = await auditTenant(tenantData.value.id, auditData);
|
||||
if (res.success) {
|
||||
const action = auditForm.result === 'approved' ? '通过' : '拒绝';
|
||||
ElMessage.success(`审核${action}成功`);
|
||||
handleClose();
|
||||
emit('success');
|
||||
} else {
|
||||
ElMessage.error(res.message || '审核失败,请重试');
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '审核失败,请重试');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
auditForm.result = '';
|
||||
auditForm.comment = '';
|
||||
auditFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
// 监听 tenant 变化
|
||||
watch(
|
||||
() => props.tenant,
|
||||
(newTenant) => {
|
||||
if (newTenant && Object.keys(newTenant).length > 0) {
|
||||
tenantData.value = {
|
||||
...newTenant,
|
||||
created_at: newTenant.create_time || newTenant.created_at,
|
||||
updated_at: newTenant.update_time || newTenant.updated_at,
|
||||
};
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.audit-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.tenant-info-section,
|
||||
.audit-form-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
border-bottom: 2px solid #4f84ff;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.audit-form-section {
|
||||
:deep(.el-form) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-radio-group) {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-radio) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<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">
|
||||
{{ tenantData.id }}
|
||||
</el-descriptions-item>
|
||||
<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>
|
||||
<el-descriptions-item label="联系电话">
|
||||
{{ tenantData.phone || '未设置' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">
|
||||
{{ tenantData.email || '未设置' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="审核状态">
|
||||
<el-tag :type="getAuditStatusType(tenantData.audit_status)">
|
||||
{{ getAuditStatusText(tenantData.audit_status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="tenantData.status === 'enabled' ? 'success' : 'info'">
|
||||
{{ tenantData.status === 'enabled' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="存储容量">
|
||||
<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;"
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ tenantData.created_at }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ tenantData.updated_at || '未更新' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ tenantData.remark || '无' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getTenantDetail } from '@/api/tenant';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
tenantId?: number | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
tenantId: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const tenantData = ref<any>({});
|
||||
|
||||
// 审核状态相关方法
|
||||
function getAuditStatusType(status: string): 'warning' | 'success' | 'danger' | 'info' {
|
||||
const statusMap: Record<string, 'warning' | 'success' | 'danger' | 'info'> = {
|
||||
pending: 'warning',
|
||||
approved: 'success',
|
||||
rejected: 'danger',
|
||||
};
|
||||
return statusMap[status] || 'info';
|
||||
}
|
||||
|
||||
function getAuditStatusText(status: string) {
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: '待审核',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
};
|
||||
return statusMap[status] || '未知';
|
||||
}
|
||||
|
||||
// 格式化容量显示(后端返回的是 MB,直接格式化)
|
||||
function formatCapacity(mb: number | undefined | null): string {
|
||||
if (mb === null || mb === undefined) {
|
||||
return '0.00 MB';
|
||||
}
|
||||
const mbValue = Number(mb);
|
||||
if (isNaN(mbValue) || mbValue < 0) {
|
||||
return '0.00 MB';
|
||||
}
|
||||
// 后端返回的是 MB,如果小于 1024 MB,直接显示 MB
|
||||
if (mbValue < 1024) {
|
||||
return `${mbValue.toFixed(2)} MB`;
|
||||
}
|
||||
// 大于等于 1024 MB,转换为 GB 显示
|
||||
const gb = mbValue / 1024;
|
||||
return `${gb.toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
// 计算容量使用百分比
|
||||
function getCapacityPercentage(capacity: number | undefined, used: number | undefined): number {
|
||||
if (!capacity || capacity === 0) return 0;
|
||||
const usedValue = used || 0;
|
||||
return Math.min(Math.round((usedValue / capacity) * 100), 100);
|
||||
}
|
||||
|
||||
// 获取容量进度条颜色
|
||||
function getCapacityColor(capacity: number | undefined, used: number | undefined): string {
|
||||
const percentage = getCapacityPercentage(capacity, used);
|
||||
if (percentage >= 90) return '#f56c6c'; // 红色
|
||||
if (percentage >= 70) return '#e6a23c'; // 橙色
|
||||
return '#67c23a'; // 绿色
|
||||
}
|
||||
|
||||
// 获取租户详情
|
||||
async function fetchDetail() {
|
||||
if (!props.tenantId) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getTenantDetail(props.tenantId);
|
||||
if (res.success && res.data) {
|
||||
const tenant = res.data;
|
||||
tenantData.value = {
|
||||
...tenant,
|
||||
created_at: tenant.create_time || tenant.created_at,
|
||||
updated_at: tenant.update_time || tenant.updated_at,
|
||||
};
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取租户详情失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || '获取租户详情失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
tenantData.value = {};
|
||||
};
|
||||
|
||||
// 监听弹窗打开和 tenantId 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal && props.tenantId) {
|
||||
fetchDetail();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.tenantId,
|
||||
() => {
|
||||
if (visible.value && props.tenantId) {
|
||||
fetchDetail();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tenant-detail {
|
||||
padding: 16px 0;
|
||||
|
||||
:deep(.el-descriptions) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions-item__label) {
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
:deep(.el-descriptions-item__content) {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑租户' : '添加租户'"
|
||||
v-model="visible"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
:model="tenantForm"
|
||||
:rules="formRules"
|
||||
ref="tenantFormRef"
|
||||
label-width="90px"
|
||||
>
|
||||
<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>
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input v-model="tenantForm.phone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="tenantForm.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="tenantForm.status" placeholder="请选择状态" style="width: 100%">
|
||||
<el-option label="启用" value="enabled" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="存储容量" prop="capacity">
|
||||
<el-input-number
|
||||
v-model="tenantForm.capacity"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="100"
|
||||
style="width: 100%"
|
||||
placeholder="请输入存储容量(MB)"
|
||||
/>
|
||||
<div class="form-tip">单位:MB,1GB = 1024MB</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="tenantForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { createTenant, updateTenant } from '@/api/tenant';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
tenant?: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
tenant: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
|
||||
const submitting = ref(false);
|
||||
const tenantFormRef = ref<FormInstance>();
|
||||
|
||||
// 判断是否为编辑模式
|
||||
const isEditing = computed(() => {
|
||||
return !!(props.tenant && props.tenant.id);
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const tenantForm = reactive({
|
||||
id: null as number | null,
|
||||
name: '',
|
||||
code: '',
|
||||
owner: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
status: 'enabled',
|
||||
capacity: 0,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
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' },
|
||||
],
|
||||
email: [{ type: 'email', message: '请输入正确的邮箱', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
||||
capacity: [
|
||||
{ required: true, message: '请输入存储容量', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '存储容量不能小于0', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
tenantForm.id = null;
|
||||
tenantForm.name = '';
|
||||
tenantForm.code = '';
|
||||
tenantForm.owner = '';
|
||||
tenantForm.phone = '';
|
||||
tenantForm.email = '';
|
||||
tenantForm.status = 'enabled';
|
||||
tenantForm.capacity = 0;
|
||||
tenantForm.remark = '';
|
||||
tenantFormRef.value?.resetFields();
|
||||
}
|
||||
|
||||
// 初始化表单数据
|
||||
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 || '';
|
||||
tenantForm.status = props.tenant.status || 'enabled';
|
||||
// capacity 后端返回的是 MB,直接使用
|
||||
tenantForm.capacity = props.tenant.capacity != null ? Number(props.tenant.capacity) : 0;
|
||||
tenantForm.remark = props.tenant.remark || '';
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!tenantFormRef.value) return;
|
||||
|
||||
await tenantFormRef.value.validate();
|
||||
submitting.value = true;
|
||||
try {
|
||||
const tenantData = {
|
||||
name: tenantForm.name,
|
||||
code: tenantForm.code,
|
||||
owner: tenantForm.owner,
|
||||
phone: tenantForm.phone || '',
|
||||
email: tenantForm.email || '',
|
||||
status: tenantForm.status,
|
||||
// capacity 前后端都使用 MB
|
||||
capacity: tenantForm.capacity || 0,
|
||||
remark: tenantForm.remark || '',
|
||||
};
|
||||
|
||||
let res;
|
||||
if (isEditing.value && tenantForm.id) {
|
||||
res = await updateTenant(tenantForm.id, tenantData);
|
||||
} else {
|
||||
res = await createTenant(tenantData);
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
if (isEditing.value) {
|
||||
ElMessage.success('租户信息已更新');
|
||||
} else {
|
||||
ElMessage.success('租户添加成功');
|
||||
}
|
||||
handleClose();
|
||||
emit('success');
|
||||
} else {
|
||||
ElMessage.error(res.message || '操作失败,请重试');
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || '操作失败,请重试');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
// 监听 tenant 变化,初始化表单
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initFormData();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.tenant,
|
||||
() => {
|
||||
if (visible.value) {
|
||||
initFormData();
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,96 +1,117 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>租户管理</h2>
|
||||
<div class="tenant-management">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">租户管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button @click="fetchTenants" title="刷新租户列表" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showAddTenantDialog = true">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加租户
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
<el-divider />
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>正在加载租户数据...</p>
|
||||
<div v-if="loading && tenants.length === 0" class="loading-state">
|
||||
<el-skeleton :rows="5" animated />
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchTenants">重试</el-button>
|
||||
<el-button type="primary" @click="fetchTenants" style="margin-top: 16px">
|
||||
重试
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 租户列表 -->
|
||||
<div v-else>
|
||||
<el-table
|
||||
:data="tenants"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
>
|
||||
<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="owner" label="负责人" width="100" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="150" />
|
||||
<el-table-column label="审核状态" width="100">
|
||||
<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="row.status === 'enabled' ? 'success' : 'info'">
|
||||
{{ row.status === 'enabled' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="success"
|
||||
v-if="row.audit_status === 'pending'"
|
||||
@click="handleAuditDialog(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>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
v-if="row.audit_status !== 'approved'"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-bar">
|
||||
<el-card shadow="never">
|
||||
<el-table
|
||||
:data="tenants"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
>
|
||||
<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="owner" label="负责人" width="100" />
|
||||
<el-table-column label="存储容量" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<div class="capacity-cell">
|
||||
<div class="capacity-info">
|
||||
<span>{{ formatCapacity(row.capacity_used ?? 0) }}</span>
|
||||
<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;"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="150" />
|
||||
<el-table-column label="审核状态" width="100">
|
||||
<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="row.status === 'enabled' ? 'success' : 'info'">
|
||||
{{ row.status === 'enabled' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</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-button>
|
||||
<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>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
v-if="row.audit_status !== 'approved'"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="page"
|
||||
@ -102,258 +123,74 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 租户详情查看弹窗 -->
|
||||
<el-dialog
|
||||
title="租户详情"
|
||||
<!-- 详情组件 -->
|
||||
<TenantDetail
|
||||
v-model="showViewDialog"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="tenant-detail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="租户ID">
|
||||
{{ currentTenant.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="租户名称">
|
||||
{{ currentTenant.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="租户编码">
|
||||
{{ currentTenant.code }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">
|
||||
{{ currentTenant.owner }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">
|
||||
{{ currentTenant.phone || '未设置' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">
|
||||
{{ currentTenant.email || '未设置' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="审核状态">
|
||||
<el-tag :type="getAuditStatusType(currentTenant.audit_status)">
|
||||
{{ getAuditStatusText(currentTenant.audit_status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="currentTenant.status === 'enabled' ? 'success' : 'info'">
|
||||
{{ currentTenant.status === 'enabled' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ currentTenant.created_at }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ currentTenant.updated_at || '未更新' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ currentTenant.remark || '无' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showViewDialog = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
:tenant-id="currentTenant?.id"
|
||||
/>
|
||||
|
||||
<!-- 审核弹窗 -->
|
||||
<el-dialog
|
||||
title="租户审核"
|
||||
<!-- 审核组件 -->
|
||||
<TenantAudit
|
||||
v-model="showAuditDialog"
|
||||
width="700px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="audit-content">
|
||||
<!-- 租户信息展示 -->
|
||||
<div class="tenant-info-section">
|
||||
<h3>租户信息</h3>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="租户ID">
|
||||
{{ currentTenant.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="租户名称">
|
||||
{{ currentTenant.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="租户编码">
|
||||
{{ currentTenant.code }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">
|
||||
{{ currentTenant.owner }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">
|
||||
{{ currentTenant.phone || '未设置' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">
|
||||
{{ currentTenant.email || '未设置' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="currentTenant.status === 'enabled' ? 'success' : 'info'">
|
||||
{{ currentTenant.status === 'enabled' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ currentTenant.created_at }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ currentTenant.remark || '无' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
:tenant="currentTenant"
|
||||
@success="handleAuditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 审核表单 -->
|
||||
<div class="audit-form-section">
|
||||
<h3>审核信息</h3>
|
||||
<el-form :model="auditForm" :rules="auditRules" ref="auditFormRef" label-width="100px">
|
||||
<el-form-item label="审核结果" prop="result">
|
||||
<el-radio-group v-model="auditForm.result">
|
||||
<el-radio value="approved">通过</el-radio>
|
||||
<el-radio value="rejected">拒绝</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核意见" prop="comment">
|
||||
<el-input
|
||||
v-model="auditForm.comment"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入审核意见"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showAuditDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitAudit">提交审核</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增/编辑租户弹窗 -->
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑租户' : '添加租户'"
|
||||
v-model="showAddTenantDialog"
|
||||
width="450px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="tenantForm" :rules="formRules" ref="tenantFormRef" label-width="90px">
|
||||
<el-form-item label="租户名称" prop="name">
|
||||
<el-input v-model="tenantForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="租户编码" prop="code">
|
||||
<el-input v-model="tenantForm.code" />
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="owner">
|
||||
<el-input v-model="tenantForm.owner" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input v-model="tenantForm.phone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="tenantForm.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="tenantForm.status">
|
||||
<el-option label="启用" value="enabled" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="tenantForm.remark" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showAddTenantDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitTenantForm">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 编辑组件 -->
|
||||
<TenantEdit
|
||||
v-model="showEditDialog"
|
||||
:tenant="currentTenant"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
|
||||
import { Plus, Check, View, Edit, Delete } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getAllTenants,
|
||||
createTenant,
|
||||
updateTenant,
|
||||
deleteTenant,
|
||||
auditTenant,
|
||||
getTenantDetail,
|
||||
} from "@/api/tenant";
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus, Check, View, Edit, Delete, Refresh } from '@element-plus/icons-vue';
|
||||
import { getAllTenants, deleteTenant } from '@/api/tenant';
|
||||
import TenantDetail from './components/detail.vue';
|
||||
import TenantAudit from './components/audit.vue';
|
||||
import TenantEdit from './components/edit.vue';
|
||||
|
||||
const tenants = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const error = ref('');
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
// 弹窗状态
|
||||
const showAddTenantDialog = ref(false);
|
||||
const showViewDialog = ref(false);
|
||||
const showAuditDialog = ref(false);
|
||||
const isEditing = ref(false);
|
||||
|
||||
// 当前查看的租户
|
||||
const currentTenant = ref<any>({});
|
||||
|
||||
// 审核表单
|
||||
const auditForm = reactive({
|
||||
result: '',
|
||||
comment: ''
|
||||
});
|
||||
const auditFormRef = ref<FormInstance>();
|
||||
|
||||
// 表单数据
|
||||
const tenantForm = reactive({
|
||||
id: null,
|
||||
name: "",
|
||||
code: "",
|
||||
owner: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
status: "enabled",
|
||||
remark: ""
|
||||
});
|
||||
const tenantFormRef = ref<FormInstance>();
|
||||
|
||||
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" }],
|
||||
email: [{ type: "email", message: "请输入正确的邮箱", trigger: "blur" }],
|
||||
status: [{ required: true, message: "请选择状态", trigger: "change" }]
|
||||
};
|
||||
|
||||
const auditRules: FormRules = {
|
||||
result: [{ required: true, message: "请选择审核结果", trigger: "change" }],
|
||||
comment: [{ required: true, message: "请输入审核意见", trigger: "blur" }]
|
||||
};
|
||||
const showEditDialog = ref(false);
|
||||
const currentTenant = ref<any>(null);
|
||||
|
||||
// 获取租户列表
|
||||
async function fetchTenants() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
error.value = '';
|
||||
try {
|
||||
const res = await getAllTenants();
|
||||
// 后端返回格式: { success: true, message: "...", data: [...] }
|
||||
if (res.success && res.data) {
|
||||
const allTenants = Array.isArray(res.data) ? res.data : [];
|
||||
// 处理字段名映射:后端返回 create_time,前端显示 created_at
|
||||
tenants.value = allTenants.map((tenant: any) => ({
|
||||
...tenant,
|
||||
created_at: tenant.create_time || tenant.created_at,
|
||||
updated_at: tenant.update_time || tenant.updated_at,
|
||||
// 确保 capacity 和 capacity_used 是数字类型
|
||||
capacity: tenant.capacity != null ? Number(tenant.capacity) : 0,
|
||||
capacity_used: tenant.capacity_used != null ? Number(tenant.capacity_used) : 0,
|
||||
}));
|
||||
total.value = tenants.value.length;
|
||||
} else {
|
||||
error.value = res.message || "获取租户列表失败";
|
||||
error.value = res.message || '获取租户列表失败';
|
||||
tenants.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || "获取租户列表失败";
|
||||
error.value = err.message || '获取租户列表失败';
|
||||
tenants.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
@ -367,12 +204,15 @@ const handlePageChange = (val: number) => {
|
||||
};
|
||||
|
||||
// 审核状态相关方法
|
||||
function getAuditStatusType(status: string): 'warning' | 'success' | 'danger' | 'info' {
|
||||
const statusMap: Record<string, 'warning' | 'success' | 'danger' | 'info'> = {
|
||||
pending: 'warning',
|
||||
approved: 'success',
|
||||
rejected: 'danger'
|
||||
};
|
||||
function getAuditStatusType(
|
||||
status: string
|
||||
): 'warning' | 'success' | 'danger' | 'info' {
|
||||
const statusMap: Record<string, 'warning' | 'success' | 'danger' | 'info'> =
|
||||
{
|
||||
pending: 'warning',
|
||||
approved: 'success',
|
||||
rejected: 'danger',
|
||||
};
|
||||
return statusMap[status] || 'info';
|
||||
}
|
||||
|
||||
@ -380,127 +220,100 @@ function getAuditStatusText(status: string) {
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: '待审核',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝'
|
||||
rejected: '已拒绝',
|
||||
};
|
||||
return statusMap[status] || '未知';
|
||||
}
|
||||
|
||||
// 查看租户详情(只读)
|
||||
async function handleView(row: any) {
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await getTenantDetail(row.id);
|
||||
if (res.success && res.data) {
|
||||
const tenant = res.data;
|
||||
currentTenant.value = {
|
||||
...tenant,
|
||||
created_at: tenant.create_time || tenant.created_at,
|
||||
updated_at: tenant.update_time || tenant.updated_at,
|
||||
};
|
||||
showViewDialog.value = true;
|
||||
} else {
|
||||
ElMessage.error(res.message || "获取租户详情失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || "获取租户详情失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
// 格式化容量显示(后端返回的是 MB,直接格式化)
|
||||
function formatCapacity(mb: number | string | undefined | null): string {
|
||||
// 处理 null/undefined/空字符串,确保是数字类型
|
||||
if (mb === null || mb === undefined || mb === '') {
|
||||
return '0.00 MB';
|
||||
}
|
||||
// 转换为数字类型
|
||||
const mbValue = typeof mb === 'string' ? parseFloat(mb) : Number(mb);
|
||||
// 检查是否为有效数字
|
||||
if (isNaN(mbValue) || mbValue < 0) {
|
||||
return '0.00 MB';
|
||||
}
|
||||
// 后端返回的是 MB,如果小于 1024 MB,直接显示 MB
|
||||
if (mbValue < 1024) {
|
||||
return `${mbValue.toFixed(2)} MB`;
|
||||
}
|
||||
// 大于等于 1024 MB,转换为 GB 显示
|
||||
const gb = mbValue / 1024;
|
||||
return `${gb.toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
// 计算容量使用百分比
|
||||
function getCapacityPercentage(capacity: number | undefined, used: number | undefined): number {
|
||||
if (!capacity || capacity === 0) return 0;
|
||||
const usedValue = used || 0;
|
||||
return Math.min(Math.round((usedValue / capacity) * 100), 100);
|
||||
}
|
||||
|
||||
// 获取容量进度条颜色
|
||||
function getCapacityColor(capacity: number | undefined, used: number | undefined): string {
|
||||
const percentage = getCapacityPercentage(capacity, used);
|
||||
if (percentage >= 90) return '#f56c6c'; // 红色
|
||||
if (percentage >= 70) return '#e6a23c'; // 橙色
|
||||
return '#67c23a'; // 绿色
|
||||
}
|
||||
|
||||
// 查看租户详情
|
||||
function handleView(row: any) {
|
||||
currentTenant.value = row;
|
||||
showViewDialog.value = true;
|
||||
}
|
||||
|
||||
// 打开审核弹窗
|
||||
function handleAuditDialog(row: any) {
|
||||
function handleAudit(row: any) {
|
||||
currentTenant.value = { ...row };
|
||||
// 重置审核表单
|
||||
auditForm.result = '';
|
||||
auditForm.comment = '';
|
||||
showAuditDialog.value = true;
|
||||
}
|
||||
|
||||
// 提交审核
|
||||
async function submitAudit() {
|
||||
await auditFormRef.value?.validate();
|
||||
loading.value = true;
|
||||
try {
|
||||
const auditData = {
|
||||
audit_status: auditForm.result,
|
||||
audit_comment: auditForm.comment,
|
||||
audit_by: getCurrentUser(),
|
||||
};
|
||||
|
||||
const res = await auditTenant(currentTenant.value.id, auditData);
|
||||
if (res.success) {
|
||||
const action = auditForm.result === 'approved' ? '通过' : '拒绝';
|
||||
ElMessage.success(`审核${action}成功`);
|
||||
showAuditDialog.value = false;
|
||||
// 重新获取租户列表
|
||||
await fetchTenants();
|
||||
} else {
|
||||
ElMessage.error(res.message || "审核失败,请重试");
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || "审核失败,请重试");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
// 审核成功回调
|
||||
function handleAuditSuccess() {
|
||||
fetchTenants();
|
||||
}
|
||||
|
||||
// 获取当前用户
|
||||
function getCurrentUser() {
|
||||
const userStr = localStorage.getItem("user");
|
||||
if (userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
return user.username || user.name || user.userName || "system";
|
||||
} catch {
|
||||
return "system";
|
||||
}
|
||||
}
|
||||
return "system";
|
||||
}
|
||||
|
||||
function resetTenantForm() {
|
||||
tenantForm.id = null;
|
||||
tenantForm.name = "";
|
||||
tenantForm.code = "";
|
||||
tenantForm.owner = "";
|
||||
tenantForm.phone = "";
|
||||
tenantForm.email = "";
|
||||
tenantForm.status = "enabled";
|
||||
tenantForm.remark = "";
|
||||
// 添加租户
|
||||
function handleAdd() {
|
||||
currentTenant.value = null;
|
||||
showEditDialog.value = true;
|
||||
}
|
||||
|
||||
// 编辑租户
|
||||
function handleEdit(row: any) {
|
||||
isEditing.value = true;
|
||||
tenantForm.id = row.id;
|
||||
tenantForm.name = row.name;
|
||||
tenantForm.code = row.code;
|
||||
tenantForm.owner = row.owner;
|
||||
tenantForm.phone = row.phone || "";
|
||||
tenantForm.email = row.email || "";
|
||||
tenantForm.status = row.status;
|
||||
tenantForm.remark = row.remark || "";
|
||||
showAddTenantDialog.value = true;
|
||||
currentTenant.value = { ...row };
|
||||
showEditDialog.value = true;
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
function handleEditSuccess() {
|
||||
fetchTenants();
|
||||
}
|
||||
|
||||
// 删除租户
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除租户「${row.name}」? 删除后不可恢复。`,
|
||||
"警告",
|
||||
{ type: "warning" }
|
||||
'警告',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await deleteTenant(row.id);
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
ElMessage.success('删除成功');
|
||||
await fetchTenants();
|
||||
} else {
|
||||
ElMessage.error(res.message || "删除失败");
|
||||
ElMessage.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || "删除失败");
|
||||
ElMessage.error(err.message || '删除失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -509,138 +322,71 @@ async function handleDelete(row: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function submitTenantForm() {
|
||||
(tenantFormRef.value as FormInstance).validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const tenantData = {
|
||||
name: tenantForm.name,
|
||||
code: tenantForm.code,
|
||||
owner: tenantForm.owner,
|
||||
phone: tenantForm.phone || "",
|
||||
email: tenantForm.email || "",
|
||||
status: tenantForm.status,
|
||||
remark: tenantForm.remark || "",
|
||||
};
|
||||
|
||||
let res;
|
||||
if (isEditing.value && tenantForm.id) {
|
||||
// 编辑租户
|
||||
res = await updateTenant(tenantForm.id, tenantData);
|
||||
} else {
|
||||
// 新增租户
|
||||
res = await createTenant(tenantData);
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
if (isEditing.value) {
|
||||
ElMessage.success("租户信息已更新");
|
||||
} else {
|
||||
ElMessage.success("租户添加成功");
|
||||
}
|
||||
showAddTenantDialog.value = false;
|
||||
resetTenantForm();
|
||||
isEditing.value = false;
|
||||
await fetchTenants();
|
||||
} else {
|
||||
ElMessage.error(res.message || "操作失败,请重试");
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || "操作失败,请重试");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchTenants();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tenant-management-module {
|
||||
padding: 20px;
|
||||
min-height: 600px;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: var(--transition-base);
|
||||
.tenant-management {
|
||||
padding: 24px;
|
||||
min-height: 100%;
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
.page-header {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.loading-state, .error-state {
|
||||
|
||||
.loading-state,
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
.loading-spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 4px solid var(--border-color);
|
||||
border-left-color: var(--primary-color);
|
||||
border-radius: var(--border-radius-full);
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
|
||||
:deep(.el-card) {
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.capacity-cell {
|
||||
.capacity-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
.capacity-divider {
|
||||
color: var(--el-text-color-placeholder);
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tenant-detail {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.tenant-detail .el-descriptions {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.tenant-detail .el-descriptions-item__label {
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.tenant-detail .el-descriptions-item__content {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.audit-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.tenant-info-section,
|
||||
.audit-form-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tenant-info-section h3,
|
||||
.audit-form-section h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.audit-form-section .el-form {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.audit-form-section .el-radio-group {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.audit-form-section .el-radio {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -14,11 +14,13 @@ type Tenant struct {
|
||||
Owner string `orm:"size(50)" json:"owner"`
|
||||
Phone string `orm:"size(20);null" json:"phone"`
|
||||
Email string `orm:"size(100);null" json:"email"`
|
||||
Status string `orm:"size(20);default(enabled)" json:"status"` // enabled, disabled
|
||||
AuditStatus string `orm:"size(20);default(pending)" json:"audit_status"` // pending, approved, rejected
|
||||
Status string `orm:"size(20);default(enabled)" 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"`
|
||||
@ -69,7 +71,7 @@ func UpdateTenant(id int, data map[string]interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteTenant 软删除租户(设置 delete_time)
|
||||
// DeleteTenant 软删除租户
|
||||
func DeleteTenant(id int) error {
|
||||
o := orm.NewOrm()
|
||||
deleteTime := time.Now()
|
||||
@ -77,7 +79,7 @@ func DeleteTenant(id int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// AuditTenant 审核租户(只能审核未删除的)
|
||||
// AuditTenant 审核租户
|
||||
func AuditTenant(id int, auditStatus, auditComment, auditBy string) error {
|
||||
o := orm.NewOrm()
|
||||
tenant := Tenant{}
|
||||
@ -99,7 +101,7 @@ func AuditTenant(id int, auditStatus, auditComment, auditBy string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetTenantById 根据ID获取租户详情(只返回未删除的)
|
||||
// GetTenantById 根据ID获取租户详情
|
||||
func GetTenantById(id int) (*Tenant, error) {
|
||||
o := orm.NewOrm()
|
||||
tenant := Tenant{}
|
||||
@ -111,7 +113,7 @@ func GetTenantById(id int) (*Tenant, error) {
|
||||
return &tenant, err
|
||||
}
|
||||
|
||||
// GetTenantByName 根据名称获取租户详情(只返回未删除的)
|
||||
// GetTenantByName 根据名称获取租户详情
|
||||
func GetTenantByName(name string) (*Tenant, error) {
|
||||
o := orm.NewOrm()
|
||||
tenant := Tenant{}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user