增加租户储存容量

This commit is contained in:
扫地僧 2025-11-02 13:17:10 +08:00
parent 01c47ccbd4
commit 494e0160b7
14 changed files with 2621 additions and 1945 deletions

View File

@ -362,7 +362,7 @@ const findMenuItemByPath = (menus, path) => {
} }
h3{ h3{
line-height: 60px; line-height: 80px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -65,28 +65,22 @@ export async function loadAndAddDynamicRoutes() {
// 创建加载 Promise // 创建加载 Promise
routesLoadingPromise = (async () => { routesLoadingPromise = (async () => {
try { try {
console.log('[路由加载] 开始从 API 加载动态路由...');
// 直接从 API 获取菜单数据 // 直接从 API 获取菜单数据
const { getAllMenus } = await import("@/api/menu"); const { getAllMenus } = await import("@/api/menu");
const res = await getAllMenus(); const res = await getAllMenus();
if (res && res.success && res.data) { if (res && res.success && res.data) {
console.log('[路由加载] API 返回菜单数量:', res.data.length);
// 添加动态路由 // 添加动态路由
addDynamicRoutes(res.data); addDynamicRoutes(res.data);
console.log('[路由加载] 动态路由加载完成');
dynamicRoutesAdded = true; dynamicRoutesAdded = true;
routesLoadingPromise = null; routesLoadingPromise = null;
return Promise.resolve(); return Promise.resolve();
} else { } else {
console.warn('[路由加载] API 返回数据格式异常:', res);
dynamicRoutesAdded = true; dynamicRoutesAdded = true;
routesLoadingPromise = null; routesLoadingPromise = null;
return Promise.resolve(); return Promise.resolve();
} }
} catch (error) { } catch (error) {
console.error('[路由加载] 加载动态路由失败:', error);
// 即使出错也标记为已加载,避免无限重试 // 即使出错也标记为已加载,避免无限重试
dynamicRoutesAdded = true; dynamicRoutesAdded = true;
routesLoadingPromise = null; routesLoadingPromise = null;
@ -100,15 +94,11 @@ export async function loadAndAddDynamicRoutes() {
// 添加动态路由到 Main 的 children 中 // 添加动态路由到 Main 的 children 中
function addDynamicRoutes(menus) { function addDynamicRoutes(menus) {
if (!menus?.length) { if (!menus?.length) {
console.warn('[路由加载] 菜单数据为空,跳过添加动态路由');
return; return;
} }
console.log('[路由加载] 开始添加动态路由,菜单数量:', menus.length);
// 如果已经添加过,先移除旧路由(刷新时可能需要重新添加) // 如果已经添加过,先移除旧路由(刷新时可能需要重新添加)
if (dynamicRoutesAdded) { if (dynamicRoutesAdded) {
console.log('[路由加载] 检测到已添加的路由,先移除旧路由');
// 移除 Main 路由以便重新添加 // 移除 Main 路由以便重新添加
if (router.hasRoute('Main')) { if (router.hasRoute('Main')) {
router.removeRoute('Main'); router.removeRoute('Main');
@ -120,18 +110,10 @@ function addDynamicRoutes(menus) {
const filteredMenus = menus.filter(menu => menu.id !== 1); const filteredMenus = menus.filter(menu => menu.id !== 1);
const dynamicRoutes = convertMenusToRoutes(filteredMenus); 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'); const mainRoute = router.getRoutes().find(r => r.name === 'Main');
if (!mainRoute) { if (!mainRoute) {
console.error('[路由加载] 找不到 Main 路由');
return; return;
} }
@ -157,7 +139,6 @@ function addDynamicRoutes(menus) {
}; };
router.addRoute(newMainRoute); router.addRoute(newMainRoute);
console.log('[路由加载] Main 路由已更新,子路由总数:', newMainRoute.children.length);
// 添加知识库的子路由(详情页和编辑页) // 添加知识库的子路由(详情页和编辑页)
// 直接在 Main 路由下添加完整路径的子路由 // 直接在 Main 路由下添加完整路径的子路由
@ -227,22 +208,16 @@ router.beforeEach(async (to, from, next) => {
// 3. 已登录,确保动态路由已加载(必须在检查 404 之前) // 3. 已登录,确保动态路由已加载(必须在检查 404 之前)
if (!dynamicRoutesAdded) { if (!dynamicRoutesAdded) {
console.log('[路由守卫] 开始加载动态路由, 目标路径:', to.path);
await loadAndAddDynamicRoutes(); await loadAndAddDynamicRoutes();
console.log('[路由守卫] 动态路由加载完成, dynamicRoutesAdded:', dynamicRoutesAdded);
// 如果路由加载后仍然未添加API 失败) // 如果路由加载后仍然未添加API 失败)
if (!dynamicRoutesAdded) { if (!dynamicRoutesAdded) {
console.warn('[路由守卫] 路由加载失败,可能是 API 错误或 token 无效');
// 重新检查 token如果 token 不存在或无效,跳转到登录页 // 重新检查 token如果 token 不存在或无效,跳转到登录页
const currentToken = localStorage.getItem('token'); const currentToken = localStorage.getItem('token');
if (!currentToken) { if (!currentToken) {
console.log('[路由守卫] 未找到 token跳转到登录页');
next({ path: "/login", query: { redirect: to.path } }); next({ path: "/login", query: { redirect: to.path } });
return; return;
} }
// 如果 token 存在但路由加载失败,可能是 token 过期或无效,清除 token 并跳转登录 // 如果 token 存在但路由加载失败,可能是 token 过期或无效,清除 token 并跳转登录
console.warn('[路由守卫] Token 存在但路由加载失败,清除 token 并跳转登录页');
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('userInfo'); localStorage.removeItem('userInfo');
next({ path: "/login", query: { redirect: to.path } }); next({ path: "/login", query: { redirect: to.path } });
@ -254,23 +229,14 @@ router.beforeEach(async (to, from, next) => {
// 重新解析路径检查是否能匹配 // 重新解析路径检查是否能匹配
const resolved = router.resolve(to.path); 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正确更新 // 如果能匹配到路由使用完整路径重新导航确保Vue Router正确更新
if (resolved.matched.length > 0 && resolved.name !== "NotFound") { if (resolved.matched.length > 0 && resolved.name !== "NotFound") {
console.log('[路由守卫] 路由匹配成功,重新导航确保正确加载');
next({ path: to.fullPath || to.path, replace: true }); next({ path: to.fullPath || to.path, replace: true });
return; return;
} }
// 如果无法匹配,使用原始路径重新导航 // 如果无法匹配,使用原始路径重新导航
console.log('[路由守卫] 路由未匹配,重新导航到:', to.path);
next({ path: to.fullPath || to.path, replace: true }); next({ path: to.fullPath || to.path, replace: true });
return; return;
} }
@ -278,29 +244,13 @@ router.beforeEach(async (to, from, next) => {
// 3.5 如果路由已加载但当前路径未匹配,可能是刷新问题 // 3.5 如果路由已加载但当前路径未匹配,可能是刷新问题
// 注意:这里需要排除根路径和已知的静态路由 // 注意:这里需要排除根路径和已知的静态路由
if (to.matched.length === 0 && to.name !== "NotFound" && to.path !== "/" && to.path !== "/dashboard") { if (to.matched.length === 0 && to.name !== "NotFound" && to.path !== "/" && to.path !== "/dashboard") {
console.log('[路由守卫] 路由已加载但未匹配,尝试重新解析:', to.path);
// 重新解析路径,看看是否能够匹配 // 重新解析路径,看看是否能够匹配
const resolved = router.resolve(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") { if (resolved.matched.length > 0 && resolved.name !== "NotFound") {
// 能够匹配,说明路由表正常,使用路径重新导航 // 能够匹配,说明路由表正常,使用路径重新导航
console.log('[路由守卫] 重新解析匹配成功,重新导航');
next({ path: to.path, replace: true }); next({ path: to.path, replace: true });
return; 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 }))
})));
} }
} }

View File

@ -4,7 +4,7 @@ import CommonHeader from '@/components/CommonHeader.vue';
import { useTabsStore } from '@/stores'; import { useTabsStore } from '@/stores';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { ref, watch, reactive, nextTick, onMounted } from 'vue'; 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 tabsStore = useTabsStore();
const router = useRouter(); const router = useRouter();
@ -175,16 +175,20 @@ function closeAllTabs() {
@contextmenu="onTabContextMenu($event, tab)" @contextmenu="onTabContextMenu($event, tab)"
/> />
</el-tabs> </el-tabs>
<!-- 跟随浮动到 tabs 最右侧的批量按钮 --> <!-- 右侧操作按钮 -->
<div class="floated-tabs-extra-btn"> <div class="tabs-extra-actions">
<el-dropdown> <el-dropdown trigger="click">
<span class="extra-action-btn"> <el-button type="primary" link size="small" class="extra-btn">
<el-icon><DArrowRight /></el-icon> <el-icon><More /></el-icon>
</span> </el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item @click="closeOthers">关闭其他</el-dropdown-item> <el-dropdown-item @click="closeOthers">
<el-dropdown-item @click="closeAll">关闭全部</el-dropdown-item> 关闭其他
</el-dropdown-item>
<el-dropdown-item @click="closeAll">
关闭全部
</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
@ -220,47 +224,132 @@ function closeAllTabs() {
.main-header { .main-header {
background-color: var(--header-bg-color, #0081ff); background-color: var(--header-bg-color, #0081ff);
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
height: 60px; height: 80px;
padding: 0; padding: 0;
} }
.right-main { .right-main {
background-color: var(--bg-color-page); background-color: var(--el-bg-color-page);
color: var(--text-color-primary); color: var(--el-text-color-primary);
transition: background-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease;
padding: 20px; padding: 20px;
overflow-y: auto; overflow-y: auto;
// overflow-y: auto fixed fixed viewport
.multi-tabs-wrapper { .multi-tabs-wrapper {
position: relative; position: relative;
zoom: 1; display: flex;
min-height: 45px;
}
.floated-tabs-extra-btn {
float: right;
margin-top: -40px;
margin-right: 12px;
z-index: 10;
}
.extra-action-btn {
display: inline-flex;
align-items: center; align-items: center;
font-size: 20px; gap: 12px;
cursor: pointer; margin-bottom: 16px;
color: #888; background: var(--el-bg-color);
background: none; border-radius: 8px;
border: none; padding: 8px 12px;
padding: 0 8px; border: 1px solid var(--el-border-color-lighter);
border-radius: 4px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: color 0.2s; }
&:hover {
color: #409eff; .multi-tabs {
background: none; 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; .tabs-extra-actions {
margin-left: 10px; flex-shrink: 0;
cursor: pointer; display: flex;
align-items: center;
.extra-btn {
padding: 8px;
font-size: 18px;
color: var(--el-text-color-regular);
&:hover {
color: var(--el-color-primary);
}
}
} }
} }
} }

View File

@ -1,13 +1,173 @@
<template> <template>
<div> <div class="category-manage">
123 <div class="page-header">
<h2 class="page-title">分类管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
添加分类
</el-button>
</div> </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> </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> </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>

View File

@ -2,68 +2,76 @@
<div class="knowledge-detail"> <div class="knowledge-detail">
<!-- 顶部标题栏 --> <!-- 顶部标题栏 -->
<div class="detail-header"> <div class="detail-header">
<el-button type="text" @click="goBack" <el-button type="primary" link @click="goBack">
><i class="fas fa-arrow-left"></i> 返回</el-button <el-icon><ArrowLeft /></el-icon>
> 返回
<h2>{{ knowledgeTitle }}</h2> </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>
<!-- 主体内容区 --> <!-- 主体内容区 -->
<div class="detail-body"> <div class="detail-body">
<!-- 左侧信息面板 --> <!-- 左侧信息面板 -->
<div class="info-panel"> <div class="info-panel">
<el-card shadow="never"> <div class="info-card">
<div slot="header" class="header"> <h3 class="info-title">基本信息</h3>
<span>基本信息</span>
</div>
<el-divider /> <el-divider />
<el-form label-width="80px" size="small"> <div class="info-content">
<el-form-item label="标题:"> <div class="info-item">
<span>{{ formData.title }}</span> <span class="info-label">标题</span>
</el-form-item> <span class="info-value">{{ formData.title }}</span>
<el-form-item label="分类:"> </div>
<el-tag size="small">{{ formData.category }}</el-tag> <div class="info-item">
</el-form-item> <span class="info-label">分类</span>
<el-form-item label="标签:"> <el-tag size="small" type="info">{{ formData.category }}</el-tag>
<el-tag </div>
v-for="tag in formData.tags" <div class="info-item">
:key="tag" <span class="info-label">标签</span>
size="small" <div class="tags-wrapper">
style="margin-right: 4px" <el-tag
>{{ tag }}</el-tag v-for="tag in formData.tags"
> :key="tag"
</el-form-item> size="small"
<el-form-item label="作者:"> effect="plain"
<span>{{ formData.author }}</span> style="margin-right: 6px; margin-bottom: 6px;"
</el-form-item> >
<el-form-item label="创建时间:"> {{ tag }}
<span>{{ formData.createTime }}</span> </el-tag>
</el-form-item> </div>
<el-form-item label="更新时间:"> </div>
<span>{{ formData.updateTime }}</span> <div class="info-item">
</el-form-item> <span class="info-label">作者</span>
</el-form> <span class="info-value">{{ formData.author }}</span>
<el-form label-width="80px" align="center"> </div>
<el-divider /> <div class="info-item">
<el-button type="primary" @click="handleEdit" <span class="info-label">创建时间</span>
><i class="fas fa-edit"></i> 编辑</el-button <span class="info-value">{{ formData.createTime }}</span>
> </div>
<el-button type="danger" @click="handleDelete" <div class="info-item">
><i class="fas fa-trash"></i> 删除</el-button <span class="info-label">更新时间</span>
> <span class="info-value">{{ formData.updateTime }}</span>
</el-form> </div>
</el-card> </div>
</div>
</div> </div>
<!-- 右侧内容面板 --> <!-- 右侧内容面板 -->
<div class="content-panel"> <div class="content-panel">
<el-card shadow="never"> <div class="content-card">
<div slot="header"> <h3 class="content-title">正文内容</h3>
<span>正文内容</span>
</div>
<el-divider /> <el-divider />
<div class="markdown-body" v-html="compiledMarkdown"></div> <div class="markdown-body" v-html="compiledMarkdown"></div>
</el-card> </div>
</div> </div>
</div> </div>
</div> </div>
@ -73,6 +81,7 @@
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Edit, Delete } from '@element-plus/icons-vue'
import { marked } from "marked" import { marked } from "marked"
import { getKnowledgeDetail, deleteKnowledge } from "@/api/knowledge" import { getKnowledgeDetail, deleteKnowledge } from "@/api/knowledge"
@ -90,6 +99,7 @@ interface FormData {
updateTime: string, updateTime: string,
content: string, content: string,
} }
const formData = ref<FormData>({ const formData = ref<FormData>({
title: "", title: "",
category: "", category: "",
@ -124,7 +134,6 @@ async function fetchDetail() {
try { try {
const idValue = id.value as string | number const idValue = id.value as string | number
const res = await getKnowledgeDetail(idValue) const res = await getKnowledgeDetail(idValue)
// API : { code: 0, data: {...}, message: "success" }
const data = (res.code === 0 && res.data) ? res.data : (res.data || res) const data = (res.code === 0 && res.data) ? res.data : (res.data || res)
formData.value = { formData.value = {
title: data.title || '', title: data.title || '',
@ -142,14 +151,17 @@ async function fetchDetail() {
ElMessage.error("获取详情失败") ElMessage.error("获取详情失败")
} }
} }
onMounted(fetchDetail) onMounted(fetchDetail)
function goBack() { function goBack() {
router.push("/apps/knowledge") router.push("/apps/knowledge")
} }
function handleEdit() { function handleEdit() {
router.push(`/apps/knowledge/edit/${id.value}`) router.push(`/apps/knowledge/edit/${id.value}`)
} }
function handleDelete() { function handleDelete() {
ElMessageBox.confirm( ElMessageBox.confirm(
"确认删除该知识?", "确认删除该知识?",
@ -163,7 +175,6 @@ function handleDelete() {
try { try {
const idValue = id.value as string | number const idValue = id.value as string | number
const res = await deleteKnowledge(idValue) const res = await deleteKnowledge(idValue)
// API : { code: 0, message: "", data: null }
if (res.code === 0) { if (res.code === 0) {
ElMessage.success("删除成功") ElMessage.success("删除成功")
goBack() goBack()
@ -178,32 +189,35 @@ function handleDelete() {
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
/* 知识详情页面样式 - 使用主题变量 */
.knowledge-detail { .knowledge-detail {
background-color: var(--bg-color-page);
min-height: 100%;
padding: 24px; padding: 24px;
transition: background-color 0.3s ease; min-height: 100%;
background-color: var(--el-bg-color-page);
} }
.detail-header { .detail-header {
background: var(--el-bg-color);
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 20px; gap: 16px;
background-color: var(--card-bg-color); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 16px 24px; border: 1px solid var(--el-border-color-lighter);
border-radius: 12px;
box-shadow: var(--box-shadow);
border: 1px solid var(--border-color-lighter);
transition: all 0.3s ease;
}
.detail-header h2 { .detail-title {
margin: 0 0 0 12px; flex: 1;
font-size: 20px; margin: 0;
font-weight: 600; font-size: 20px;
color: var(--text-color-primary); font-weight: 600;
transition: color 0.3s ease; color: var(--el-text-color-primary);
}
.header-actions {
display: flex;
gap: 12px;
}
} }
.detail-body { .detail-body {
@ -215,94 +229,120 @@ function handleDelete() {
width: 320px; width: 320px;
flex-shrink: 0; flex-shrink: 0;
:deep(.el-card) { .info-card {
background-color: var(--card-bg-color); background: var(--el-bg-color);
border-color: var(--border-color-lighter); border-radius: 12px;
transition: all 0.3s ease; padding: 24px;
} box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1px solid var(--el-border-color-lighter);
.header { .info-title {
color: var(--text-color-primary); margin: 0 0 16px;
font-weight: 600; font-size: 18px;
transition: color 0.3s ease; font-weight: 600;
} color: var(--el-text-color-primary);
}
:deep(.el-form-item__label) { .info-content {
color: var(--text-color-secondary); .info-item {
transition: color 0.3s ease; margin-bottom: 20px;
}
:deep(.el-form-item__content) { &:last-child {
color: var(--text-color-primary); margin-bottom: 0;
transition: color 0.3s ease; }
}
:deep(.el-tag) { .info-label {
background-color: var(--fill-color-light); display: block;
border-color: var(--border-color-lighter); font-size: 13px;
color: var(--text-color-primary); color: var(--el-text-color-regular);
transition: all 0.3s ease; 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 { .content-panel {
flex: 1; flex: 1;
min-width: 0;
:deep(.el-card) { .content-card {
background-color: var(--card-bg-color); background: var(--el-bg-color);
border-color: var(--border-color-lighter); border-radius: 12px;
transition: all 0.3s ease; 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) { .content-title {
color: var(--text-color-primary); margin: 0 0 16px;
border-bottom-color: var(--border-color-lighter); font-size: 18px;
transition: all 0.3s ease; font-weight: 600;
color: var(--el-text-color-primary);
}
} }
} }
.markdown-body { .markdown-body {
font-size: 14px; font-size: 15px;
line-height: 1.8; line-height: 1.8;
color: var(--text-color-primary); color: var(--el-text-color-primary);
transition: color 0.3s ease; min-height: 400px;
// Markdown
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) { :deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
color: var(--text-color-primary); color: var(--el-text-color-primary);
margin-top: 16px; margin-top: 24px;
margin-bottom: 8px; 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) { :deep(p), :deep(li), :deep(span), :deep(div) {
color: var(--text-color-primary); color: var(--el-text-color-primary);
margin: 8px 0; margin: 12px 0;
} }
:deep(a) { :deep(a) {
color: var(--primary-color); color: #4f84ff;
text-decoration: none; text-decoration: none;
&:hover { &:hover {
opacity: 0.8; text-decoration: underline;
} }
} }
:deep(code) { :deep(code) {
background-color: var(--fill-color-light); background-color: var(--el-fill-color-light);
color: var(--text-color-primary); color: var(--el-text-color-primary);
border-color: var(--border-color-lighter); border: 1px solid var(--el-border-color-lighter);
padding: 2px 6px; padding: 2px 6px;
border-radius: 3px; border-radius: 4px;
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
font-size: 14px;
} }
:deep(pre) { :deep(pre) {
background-color: var(--fill-color-light); background-color: var(--el-fill-color-light);
border-color: var(--border-color-lighter); border: 1px solid var(--el-border-color-lighter);
padding: 12px; padding: 16px;
border-radius: 8px; border-radius: 8px;
overflow-x: auto; overflow-x: auto;
margin: 16px 0;
code { code {
background-color: transparent; background-color: transparent;
@ -314,21 +354,24 @@ function handleDelete() {
:deep(img) { :deep(img) {
max-width: 100%; max-width: 100%;
border-radius: 8px; border-radius: 8px;
margin: 8px 0; margin: 16px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
:deep(ul), :deep(ol) { :deep(ul), :deep(ol) {
color: var(--text-color-primary); color: var(--el-text-color-primary);
margin: 8px 0; margin: 12px 0;
padding-left: 24px; padding-left: 24px;
} }
:deep(blockquote) { :deep(blockquote) {
border-left: 4px solid var(--primary-color); border-left: 4px solid #4f84ff;
padding-left: 16px; padding-left: 16px;
margin: 16px 0; margin: 16px 0;
color: var(--text-color-secondary); color: var(--el-text-color-regular);
background-color: var(--fill-color-extra-light); background-color: var(--el-fill-color-lighter);
padding: 12px 16px;
border-radius: 4px;
} }
:deep(table) { :deep(table) {
@ -337,28 +380,23 @@ function handleDelete() {
margin: 16px 0; margin: 16px 0;
th, td { th, td {
border: 1px solid var(--border-color-lighter); border: 1px solid var(--el-border-color-lighter);
padding: 8px 12px; padding: 12px;
color: var(--text-color-primary); color: var(--el-text-color-primary);
text-align: left;
} }
th { th {
background-color: var(--fill-color-light); background-color: var(--el-fill-color-light);
font-weight: 600; font-weight: 600;
} }
} }
} }
.actions {
margin-top: 16px;
text-align: center;
}
/* 响应式布局 */ /* 响应式布局 */
@media (max-width: 768px) { @media (max-width: 768px) {
.detail-body { .detail-body {
flex-direction: column; flex-direction: column;
padding: 0 16px 16px;
} }
.info-panel { .info-panel {
@ -366,11 +404,14 @@ function handleDelete() {
} }
.detail-header { .detail-header {
padding: 12px 16px; flex-direction: column;
} align-items: flex-start;
gap: 12px;
.detail-header h2 { .header-actions {
font-size: 16px; width: 100%;
justify-content: flex-end;
}
} }
} }
</style> </style>

View File

@ -2,15 +2,18 @@
<div class="knowledge-edit"> <div class="knowledge-edit">
<!-- 顶部标题栏 --> <!-- 顶部标题栏 -->
<div class="edit-header"> <div class="edit-header">
<el-button type="text" @click="goBack" <el-button type="primary" link @click="goBack">
><i class="fas fa-arrow-left"></i> 返回</el-button <el-icon><ArrowLeft /></el-icon>
> 返回
<h2>{{ isEdit ? "编辑知识" : "新建知识" }}</h2> </el-button>
<h2 class="edit-title">{{ isEdit ? "编辑知识" : "新建知识" }}</h2>
</div> </div>
<!-- 基本信息 --> <!-- 基本信息 -->
<div class="edit-meta"> <div class="edit-meta">
<el-card shadow="never"> <div class="meta-card">
<h3 class="meta-title">基本信息</h3>
<el-divider />
<el-form <el-form
:model="formData" :model="formData"
:rules="rules" :rules="rules"
@ -18,16 +21,16 @@
label-width="80px" label-width="80px"
size="default" size="default"
> >
<el-row :gutter="20" style="margin-bottom: 20px;"> <el-row :gutter="20">
<el-col :span="24"> <el-col :span="24">
<el-form-item label="标题" prop="title"> <el-form-item label="标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入标题" /> <el-input v-model="formData.title" placeholder="请输入标题" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="6"> <el-col :span="6">
<el-form-item label="分类" prop="category"> <el-form-item label="分类" prop="category">
<el-select <el-select
v-model="formData.category" v-model="formData.category"
placeholder="请选择分类" placeholder="请选择分类"
@ -44,7 +47,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="标签" prop="tags"> <el-form-item label="标签" prop="tags">
<el-select <el-select
v-model="formData.tags" v-model="formData.tags"
multiple multiple
@ -63,12 +66,12 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="作者" prop="author"> <el-form-item label="作者" prop="author">
<el-input v-model="formData.author" placeholder="请输入作者" /> <el-input v-model="formData.author" placeholder="请输入作者" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <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-group v-model="formData.share">
<el-radio-button :label="0">个人</el-radio-button> <el-radio-button :label="0">个人</el-radio-button>
<el-radio-button :label="1">共享</el-radio-button> <el-radio-button :label="1">共享</el-radio-button>
@ -77,35 +80,35 @@
</el-col> </el-col>
</el-row> </el-row>
</el-form> </el-form>
<el-divider />
<div class="meta-actions"> <div class="meta-actions">
<el-button type="primary" @click="handleSubmit" <el-button type="primary" @click="handleSubmit">
><i class="fas fa-save"></i> 保存</el-button <el-icon><Check /></el-icon>
> 保存
</el-button>
<el-button @click="goBack">取消</el-button> <el-button @click="goBack">取消</el-button>
</div> </div>
</el-card> </div>
</div> </div>
<!-- 正文内容区 - 左右结构 --> <!-- 正文内容区 - 左右结构 -->
<div class="edit-body"> <div class="edit-body">
<!-- 左侧编辑器 --> <!-- 左侧编辑器 -->
<div class="editor-panel"> <div class="editor-panel">
<el-card shadow="never"> <div class="editor-card">
<template #header> <h3 class="editor-title">编辑正文</h3>
<span>编辑正文</span> <el-divider />
</template> <div class="editor-content">
<WangEditor v-model="formData.content" /> <WangEditor v-model="formData.content" />
</el-card> </div>
</div>
</div> </div>
<!-- 右侧预览 --> <!-- 右侧预览 -->
<div class="preview-panel"> <div class="preview-panel">
<el-card shadow="never"> <div class="preview-card">
<template #header> <h3 class="preview-title">预览效果</h3>
<span>预览效果</span> <el-divider />
</template>
<div class="markdown-body" v-html="compiledMarkdown"></div> <div class="markdown-body" v-html="compiledMarkdown"></div>
</el-card> </div>
</div> </div>
</div> </div>
</div> </div>
@ -115,18 +118,18 @@
import { ref, reactive, computed, onMounted, watch } from "vue"; import { ref, reactive, computed, onMounted, watch } from "vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { marked } from "marked"; import { marked } from "marked";
import { ArrowLeft, Check } from '@element-plus/icons-vue'
// @ts-ignore // @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 { ElMessage, ElMessageBox } from "element-plus";
import type { FormInstance, FormRules } from "element-plus"; import type { FormInstance, FormRules } from "element-plus";
import WangEditor from '@/views/components/WangEditor.vue';
// DOM
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
//
const formData = reactive<{ const formData = reactive<{
title: string; title: string;
category: string; category: string;
@ -145,7 +148,6 @@ const formData = reactive<{
share: 0, share: 0,
}); });
//
const rules = reactive<FormRules>({ const rules = reactive<FormRules>({
title: [{ required: true, message: "请输入标题", trigger: "blur" }], title: [{ required: true, message: "请输入标题", trigger: "blur" }],
category: [{ required: true, message: "请选择分类", trigger: "change" }], category: [{ required: true, message: "请选择分类", trigger: "change" }],
@ -153,7 +155,6 @@ const rules = reactive<FormRules>({
content: [{ required: true, message: "请输入正文", trigger: "blur" }], content: [{ required: true, message: "请输入正文", trigger: "blur" }],
}); });
//
interface CategoryItem { interface CategoryItem {
categoryId: number; categoryId: number;
categoryName: string; categoryName: string;
@ -161,18 +162,14 @@ interface CategoryItem {
const categoryList = ref<CategoryItem[]>([]); const categoryList = ref<CategoryItem[]>([]);
const tagList = ref<string[]>([]); const tagList = ref<string[]>([]);
// id: 'new'
const id = computed(() => route.params.id || route.query.id); const id = computed(() => route.params.id || route.query.id);
const isEdit = computed(() => { const isEdit = computed(() => {
const currentId = id.value; const currentId = id.value;
return !!currentId && currentId !== "new" && currentId !== ""; return !!currentId && currentId !== "new" && currentId !== "";
}); });
// markdown
const compiledMarkdown = computed(() => marked(formData.content || "")); const compiledMarkdown = computed(() => marked(formData.content || ""));
// TODO: wangEditor
// 使 textarea
watch( watch(
() => formData.content, () => formData.content,
() => { () => {
@ -180,7 +177,6 @@ watch(
} }
); );
//
const getLoginUser = () => { const getLoginUser = () => {
const userStr = localStorage.getItem("user"); const userStr = localStorage.getItem("user");
if (userStr) { if (userStr) {
@ -194,25 +190,20 @@ const getLoginUser = () => {
return ""; return "";
}; };
//
const fetchDetail = async () => { const fetchDetail = async () => {
try { try {
const currentId = id.value as string; const currentId = id.value as string;
if (currentId && currentId !== "new") { if (currentId && currentId !== "new") {
const res = await getKnowledgeDetail(parseInt(currentId as string)); // Changed to getKnowledgeDetail const res = await getKnowledgeDetail(parseInt(currentId as string));
// API : { code: 0, data: {...}, message: "success" }
const data = res.code === 0 && res.data ? res.data : res.data || res; const data = res.code === 0 && res.data ? res.data : res.data || res;
//
// categoryName
formData.title = data.title || ""; formData.title = data.title || "";
formData.category = data.categoryName || ""; formData.category = data.categoryName || "";
formData.categoryId = data.categoryId || 0; formData.categoryId = data.categoryId || 0;
formData.author = data.author || ""; formData.author = data.author || "";
formData.content = data.content || ""; formData.content = data.content || "";
formData.share = data.share || 0; // Added share loading formData.share = data.share || 0;
// categoryId categoryName
if (!formData.categoryId && formData.category) { if (!formData.categoryId && formData.category) {
const foundCategory = categoryList.value.find( const foundCategory = categoryList.value.find(
(item) => item.categoryName === formData.category (item) => item.categoryName === formData.category
@ -222,14 +213,12 @@ const fetchDetail = async () => {
} }
} }
// Tags JSON
if (data.tags) { if (data.tags) {
try { try {
const parsed = const parsed =
typeof data.tags === "string" ? JSON.parse(data.tags) : data.tags; typeof data.tags === "string" ? JSON.parse(data.tags) : data.tags;
formData.tags = Array.isArray(parsed) ? parsed : []; formData.tags = Array.isArray(parsed) ? parsed : [];
} catch { } catch {
//
formData.tags = Array.isArray(data.tags) ? data.tags : []; formData.tags = Array.isArray(data.tags) ? data.tags : [];
} }
} else { } else {
@ -241,7 +230,6 @@ const fetchDetail = async () => {
} }
}; };
//
const loadCategoryAndTag = async () => { const loadCategoryAndTag = async () => {
try { try {
const [catRes, tagRes] = await Promise.all([ const [catRes, tagRes] = await Promise.all([
@ -249,14 +237,12 @@ const loadCategoryAndTag = async () => {
getTagList(), getTagList(),
]); ]);
// API : { code: 0, data: [...], message: "success" }
const categories = const categories =
catRes.code === 0 && catRes.data ? catRes.data : catRes.data || []; catRes.code === 0 && catRes.data ? catRes.data : catRes.data || [];
const tags = const tags =
tagRes.code === 0 && tagRes.data ? tagRes.data : tagRes.data || []; tagRes.code === 0 && tagRes.data ? tagRes.data : tagRes.data || [];
categoryList.value = Array.isArray(categories) ? categories : []; categoryList.value = Array.isArray(categories) ? categories : [];
// { tagId, tagName } tagName
tagList.value = Array.isArray(tags) tagList.value = Array.isArray(tags)
? tags.map((tag) => tag.tagName || tag) ? tags.map((tag) => tag.tagName || tag)
: []; : [];
@ -267,7 +253,6 @@ const loadCategoryAndTag = async () => {
} }
}; };
//
const handleCategoryChange = (categoryName: string) => { const handleCategoryChange = (categoryName: string) => {
const selectedCategory = categoryList.value.find( const selectedCategory = categoryList.value.find(
(item) => item.categoryName === categoryName (item) => item.categoryName === categoryName
@ -277,12 +262,10 @@ const handleCategoryChange = (categoryName: string) => {
} }
}; };
//
const goBack = () => { const goBack = () => {
router.push("/apps/knowledge"); router.push("/apps/knowledge");
}; };
//
const handleSubmit = () => { const handleSubmit = () => {
if (!formRef.value) return; if (!formRef.value) return;
formRef.value.validate(async (valid: boolean) => { formRef.value.validate(async (valid: boolean) => {
@ -290,14 +273,11 @@ const handleSubmit = () => {
const currentId = id.value as string; const currentId = id.value as string;
// categoryId
if (!formData.categoryId) { if (!formData.categoryId) {
ElMessage.warning("请选择分类"); ElMessage.warning("请选择分类");
return; return;
} }
// JSON tag
// categoryId
const submitData: any = { const submitData: any = {
title: formData.title || "", title: formData.title || "",
categoryId: Number(formData.categoryId) || 0, categoryId: Number(formData.categoryId) || 0,
@ -307,21 +287,18 @@ const handleSubmit = () => {
Array.isArray(formData.tags) && formData.tags.length > 0 Array.isArray(formData.tags) && formData.tags.length > 0
? JSON.stringify(formData.tags) ? JSON.stringify(formData.tags)
: "[]", : "[]",
status: 1, // status: 1,
share: formData.share, // Added share in params share: formData.share,
}; };
if (isEdit.value && currentId !== "new") { if (isEdit.value && currentId !== "new") {
// id JSON tag "id"
const knowledgeId = parseInt(currentId as string); const knowledgeId = parseInt(currentId as string);
if (isNaN(knowledgeId) || knowledgeId <= 0) { if (isNaN(knowledgeId) || knowledgeId <= 0) {
ElMessage.error("知识ID无效"); ElMessage.error("知识ID无效");
return; return;
} }
// updateKnowledge id data
try { try {
const res = await updateKnowledge(knowledgeId, submitData); const res = await updateKnowledge(knowledgeId, submitData);
// API : { code: 0, message: "", data: null }
if (res.code === 0) { if (res.code === 0) {
ElMessage.success("保存成功"); ElMessage.success("保存成功");
goBack(); goBack();
@ -335,10 +312,8 @@ const handleSubmit = () => {
ElMessage.error(errorMessage); ElMessage.error(errorMessage);
} }
} else { } else {
// id
try { try {
const res = await createKnowledge(submitData); const res = await createKnowledge(submitData);
// API : { code: 0, message: "", data: { id: ... } }
if (res.code === 0) { if (res.code === 0) {
ElMessage.success("创建成功"); ElMessage.success("创建成功");
goBack(); 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 () => { onMounted(async () => {
//
const author = getLoginUser(); const author = getLoginUser();
if (author && !isEdit.value) { if (author && !isEdit.value) {
formData.author = author; formData.author = author;
} }
//
await loadCategoryAndTag(); await loadCategoryAndTag();
//
if (isEdit.value) { if (isEdit.value) {
await fetchDetail(); await fetchDetail();
} else { } else {
//
if (categoryList.value.length > 0 && !formData.category) { if (categoryList.value.length > 0 && !formData.category) {
formData.category = categoryList.value[0].categoryName; formData.category = categoryList.value[0].categoryName;
formData.categoryId = categoryList.value[0].categoryId; formData.categoryId = categoryList.value[0].categoryId;
@ -397,7 +348,6 @@ onMounted(async () => {
} }
}); });
//
defineExpose({ defineExpose({
formRef, formRef,
}); });
@ -405,165 +355,123 @@ defineExpose({
<style scoped lang="less"> <style scoped lang="less">
.knowledge-edit { .knowledge-edit {
background-color: var(--bg-color-page);
min-height: 100%;
padding: 24px; padding: 24px;
transition: background-color 0.3s ease; min-height: 100%;
background-color: var(--el-bg-color-page);
} }
.edit-header { .edit-header {
background: var(--el-bg-color);
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 20px; gap: 16px;
background-color: var(--card-bg-color); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 16px 24px; border: 1px solid var(--el-border-color-lighter);
border-radius: 12px;
box-shadow: var(--box-shadow); .edit-title {
border: 1px solid var(--border-color-lighter); margin: 0;
transition: all 0.3s ease; 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 { .edit-meta {
margin-bottom: 20px; margin-bottom: 20px;
:deep(.el-card) { .meta-card {
background-color: var(--card-bg-color); background: var(--el-bg-color);
border-color: var(--border-color-lighter); border-radius: 12px;
transition: all 0.3s ease; 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) { .meta-title {
color: var(--text-color-primary); margin: 0 0 16px;
border-bottom-color: var(--border-color-lighter); font-size: 18px;
transition: all 0.3s ease; font-weight: 600;
} color: var(--el-text-color-primary);
}
:deep(.el-divider) { .meta-actions {
border-color: var(--border-color-lighter); margin-top: 24px;
transition: border-color 0.3s ease; text-align: right;
}
} }
} }
.meta-actions {
margin-top: 16px;
text-align: right;
}
/* 正文编辑区 - 左右结构 */
.edit-body { .edit-body {
display: flex; display: flex;
gap: 20px; gap: 20px;
min-height: 600px; min-height: 600px;
} }
.editor-panel { .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;
}
}
.preview-panel { .preview-panel {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
}
:deep(.el-card) { .editor-card,
background-color: var(--card-bg-color); .preview-card {
border-color: var(--border-color-lighter); background: var(--el-bg-color);
transition: all 0.3s ease; border-radius: 12px;
height: 100%; 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) { .editor-content {
color: var(--text-color-primary); flex: 1;
border-bottom-color: var(--border-color-lighter); min-height: 400px;
transition: all 0.3s ease;
} }
} }
.markdown-body { .preview-card {
font-size: 14px; .markdown-body {
line-height: 1.8; flex: 1;
color: var(--text-color-primary); overflow-y: auto;
min-height: 400px; font-size: 14px;
padding: 14px 16px; line-height: 1.8;
transition: color 0.3s ease; color: var(--el-text-color-primary);
padding: 16px;
:deep(h1), background: var(--el-fill-color-lighter);
: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;
border-radius: 8px; border-radius: 8px;
overflow-x: auto;
code { :deep(h1), :deep(h2), :deep(h3) {
background-color: transparent; margin-top: 16px;
border: none; margin-bottom: 8px;
padding: 0;
} }
}
:deep(img) { :deep(p), :deep(li) {
max-width: 100%; margin: 8px 0;
border-radius: 8px; }
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 { .preview-panel {
width: 100%; 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> </style>

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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">单位MB1GB = 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>

View File

@ -1,96 +1,117 @@
<template> <template>
<div class="container-box"> <div class="tenant-management">
<div class="header-bar"> <div class="page-header">
<h2>租户管理</h2> <h2 class="page-title">租户管理</h2>
<div class="header-actions"> <div class="header-actions">
<el-button @click="fetchTenants" title="刷新租户列表" :loading="loading"> <el-button @click="fetchTenants" title="刷新租户列表" :loading="loading">
<el-icon><Refresh /></el-icon> <el-icon><Refresh /></el-icon>
刷新 刷新
</el-button> </el-button>
<el-button type="primary" @click="showAddTenantDialog = true"> <el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
添加租户 添加租户
</el-button> </el-button>
</div> </div>
</div> </div>
<el-divider></el-divider> <el-divider />
<!-- 加载状态 --> <!-- 加载状态 -->
<div v-if="loading" class="loading-state"> <div v-if="loading && tenants.length === 0" class="loading-state">
<div class="loading-spinner"></div> <el-skeleton :rows="5" animated />
<p>正在加载租户数据...</p>
</div> </div>
<!-- 错误状态 --> <!-- 错误状态 -->
<div v-else-if="error" class="error-state"> <div v-else-if="error" class="error-state">
<el-alert title="加载失败" :message="error" type="error" show-icon /> <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>
<!-- 租户列表 --> <!-- 租户列表 -->
<div v-else> <div v-else>
<el-table <el-card shadow="never">
:data="tenants" <el-table
stripe :data="tenants"
style="width: 100%" stripe
v-loading="loading" 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="id" label="ID" width="80" align="center" />
<el-table-column prop="code" label="租户编码" width="120" /> <el-table-column prop="name" label="租户名称" min-width="120" />
<el-table-column prop="owner" label="负责人" width="100" /> <el-table-column prop="code" label="租户编码" width="120" />
<el-table-column prop="created_at" label="创建时间" width="150" /> <el-table-column prop="owner" label="负责人" width="100" />
<el-table-column label="审核状态" width="100"> <el-table-column label="存储容量" min-width="240">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="getAuditStatusType(row.audit_status)"> <div class="capacity-cell">
{{ getAuditStatusText(row.audit_status) }} <div class="capacity-info">
</el-tag> <span>{{ formatCapacity(row.capacity_used ?? 0) }}</span>
</template> <span class="capacity-divider">/</span>
</el-table-column> <span>{{ formatCapacity(row.capacity ?? 0) }}</span>
<el-table-column label="状态" width="80"> </div>
<template #default="{ row }"> <el-progress
<el-tag :type="row.status === 'enabled' ? 'success' : 'info'"> :percentage="getCapacityPercentage(row.capacity, row.capacity_used)"
{{ row.status === 'enabled' ? '启用' : '禁用' }} :color="getCapacityColor(row.capacity, row.capacity_used)"
</el-tag> :stroke-width="4"
</template> style="margin-top: 4px;"
</el-table-column> />
<el-table-column label="操作" width="300" align="center"> </div>
<template #default="{ row }"> </template>
<el-button size="small" @click="handleView(row)"> </el-table-column>
<el-icon><View /></el-icon> <el-table-column prop="created_at" label="创建时间" width="150" />
查看 <el-table-column label="审核状态" width="100">
</el-button> <template #default="{ row }">
<el-button <el-tag :type="getAuditStatusType(row.audit_status)">
size="small" {{ getAuditStatusText(row.audit_status) }}
type="success" </el-tag>
v-if="row.audit_status === 'pending'" </template>
@click="handleAuditDialog(row)" </el-table-column>
> <el-table-column label="状态" width="80">
<el-icon><Check /></el-icon> <template #default="{ row }">
审核 <el-tag :type="row.status === 'enabled' ? 'success' : 'info'">
</el-button> {{ row.status === 'enabled' ? '启用' : '禁用' }}
<el-button </el-tag>
size="small" </template>
@click="handleEdit(row)" </el-table-column>
v-if="row.audit_status !== 'approved'" <el-table-column label="操作" width="300" align="center" fixed="right">
> <template #default="{ row }">
<el-icon><Edit /></el-icon> <el-button size="small" @click="handleView(row)">
编辑 <el-icon><View /></el-icon>
</el-button> 查看
<el-button </el-button>
size="small" <el-button
type="danger" size="small"
@click="handleDelete(row)" type="success"
v-if="row.audit_status !== 'approved'" v-if="row.audit_status === 'pending'"
> @click="handleAudit(row)"
<el-icon><Delete /></el-icon> >
删除 <el-icon><Check /></el-icon>
</el-button> 审核
</template> </el-button>
</el-table-column> <el-button
</el-table> size="small"
<div class="pagination-bar"> @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 <el-pagination
background background
:current-page="page" :current-page="page"
@ -102,258 +123,74 @@
</div> </div>
</div> </div>
<!-- 租户详情查看弹窗 --> <!-- 详情组件 -->
<el-dialog <TenantDetail
title="租户详情"
v-model="showViewDialog" v-model="showViewDialog"
width="600px" :tenant-id="currentTenant?.id"
: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>
<!-- 审核弹窗 --> <!-- 审核组件 -->
<el-dialog <TenantAudit
title="租户审核"
v-model="showAuditDialog" v-model="showAuditDialog"
width="700px" :tenant="currentTenant"
:close-on-click-modal="false" @success="handleAuditSuccess"
> />
<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>
<!-- 审核表单 --> <!-- 编辑组件 -->
<div class="audit-form-section"> <TenantEdit
<h3>审核信息</h3> v-model="showEditDialog"
<el-form :model="auditForm" :rules="auditRules" ref="auditFormRef" label-width="100px"> :tenant="currentTenant"
<el-form-item label="审核结果" prop="result"> @success="handleEditSuccess"
<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>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from "vue"; import { ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus"; import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus, Check, View, Edit, Delete } from "@element-plus/icons-vue"; import { Plus, Check, View, Edit, Delete, Refresh } from '@element-plus/icons-vue';
import { import { getAllTenants, deleteTenant } from '@/api/tenant';
getAllTenants, import TenantDetail from './components/detail.vue';
createTenant, import TenantAudit from './components/audit.vue';
updateTenant, import TenantEdit from './components/edit.vue';
deleteTenant,
auditTenant,
getTenantDetail,
} from "@/api/tenant";
const tenants = ref<any[]>([]); const tenants = ref<any[]>([]);
const loading = ref(false); const loading = ref(false);
const error = ref(""); const error = ref('');
const page = ref(1); const page = ref(1);
const pageSize = ref(10); const pageSize = ref(10);
const total = ref(0); const total = ref(0);
// //
const showAddTenantDialog = ref(false);
const showViewDialog = ref(false); const showViewDialog = ref(false);
const showAuditDialog = ref(false); const showAuditDialog = ref(false);
const isEditing = ref(false); const showEditDialog = ref(false);
const currentTenant = ref<any>(null);
//
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" }]
};
//
async function fetchTenants() { async function fetchTenants() {
loading.value = true; loading.value = true;
error.value = ""; error.value = '';
try { try {
const res = await getAllTenants(); const res = await getAllTenants();
// : { success: true, message: "...", data: [...] }
if (res.success && res.data) { if (res.success && res.data) {
const allTenants = Array.isArray(res.data) ? res.data : []; const allTenants = Array.isArray(res.data) ? res.data : [];
// create_time created_at
tenants.value = allTenants.map((tenant: any) => ({ tenants.value = allTenants.map((tenant: any) => ({
...tenant, ...tenant,
created_at: tenant.create_time || tenant.created_at, created_at: tenant.create_time || tenant.created_at,
updated_at: tenant.update_time || tenant.updated_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; total.value = tenants.value.length;
} else { } else {
error.value = res.message || "获取租户列表失败"; error.value = res.message || '获取租户列表失败';
tenants.value = []; tenants.value = [];
total.value = 0; total.value = 0;
} }
} catch (err: any) { } catch (err: any) {
error.value = err.message || "获取租户列表失败"; error.value = err.message || '获取租户列表失败';
tenants.value = []; tenants.value = [];
total.value = 0; total.value = 0;
} finally { } finally {
@ -367,12 +204,15 @@ const handlePageChange = (val: number) => {
}; };
// //
function getAuditStatusType(status: string): 'warning' | 'success' | 'danger' | 'info' { function getAuditStatusType(
const statusMap: Record<string, 'warning' | 'success' | 'danger' | 'info'> = { status: string
pending: 'warning', ): 'warning' | 'success' | 'danger' | 'info' {
approved: 'success', const statusMap: Record<string, 'warning' | 'success' | 'danger' | 'info'> =
rejected: 'danger' {
}; pending: 'warning',
approved: 'success',
rejected: 'danger',
};
return statusMap[status] || 'info'; return statusMap[status] || 'info';
} }
@ -380,127 +220,100 @@ function getAuditStatusText(status: string) {
const statusMap: Record<string, string> = { const statusMap: Record<string, string> = {
pending: '待审核', pending: '待审核',
approved: '已通过', approved: '已通过',
rejected: '已拒绝' rejected: '已拒绝',
}; };
return statusMap[status] || '未知'; return statusMap[status] || '未知';
} }
// // MB
async function handleView(row: any) { function formatCapacity(mb: number | string | undefined | null): string {
try { // null/undefined/
loading.value = true; if (mb === null || mb === undefined || mb === '') {
const res = await getTenantDetail(row.id); return '0.00 MB';
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;
} }
//
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 }; currentTenant.value = { ...row };
//
auditForm.result = '';
auditForm.comment = '';
showAuditDialog.value = true; showAuditDialog.value = true;
} }
// //
async function submitAudit() { function handleAuditSuccess() {
await auditFormRef.value?.validate(); fetchTenants();
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 getCurrentUser() { function handleAdd() {
const userStr = localStorage.getItem("user"); currentTenant.value = null;
if (userStr) { showEditDialog.value = true;
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 handleEdit(row: any) { function handleEdit(row: any) {
isEditing.value = true; currentTenant.value = { ...row };
tenantForm.id = row.id; showEditDialog.value = true;
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;
} }
//
function handleEditSuccess() {
fetchTenants();
}
//
async function handleDelete(row: any) { async function handleDelete(row: any) {
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
`确定要删除租户「${row.name}」? 删除后不可恢复。`, `确定要删除租户「${row.name}」? 删除后不可恢复。`,
"警告", '警告',
{ type: "warning" } { type: 'warning' }
); );
loading.value = true; loading.value = true;
try { try {
const res = await deleteTenant(row.id); const res = await deleteTenant(row.id);
if (res.success) { if (res.success) {
ElMessage.success("删除成功"); ElMessage.success('删除成功');
await fetchTenants(); await fetchTenants();
} else { } else {
ElMessage.error(res.message || "删除失败"); ElMessage.error(res.message || '删除失败');
} }
} catch (err: any) { } catch (err: any) {
ElMessage.error(err.message || "删除失败"); ElMessage.error(err.message || '删除失败');
} finally { } finally {
loading.value = false; 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(() => { onMounted(() => {
fetchTenants(); fetchTenants();
}); });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.tenant-management-module { .tenant-management {
padding: 20px; padding: 24px;
min-height: 600px; min-height: 100%;
background: var(--card-bg); background-color: var(--el-bg-color-page);
border-radius: var(--border-radius-lg);
box-shadow: var(--card-shadow);
transition: var(--transition-base);
} }
.header-actions { .page-header {
display: flex; display: flex;
gap: 8px; justify-content: space-between;
align-items: center; 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; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 40px 0; padding: 40px 0;
} }
.loading-spinner {
width: 36px; .pagination-wrapper {
height: 36px; margin-top: 20px;
border: 4px solid var(--border-color); display: flex;
border-left-color: var(--primary-color); justify-content: flex-end;
border-radius: var(--border-radius-full);
animation: spin 1s linear infinite;
margin-bottom: 16px;
} }
@keyframes spin {
100% { :deep(.el-card) {
transform: rotate(360deg); 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> </style>

View File

@ -14,11 +14,13 @@ type Tenant struct {
Owner string `orm:"size(50)" json:"owner"` Owner string `orm:"size(50)" json:"owner"`
Phone string `orm:"size(20);null" json:"phone"` Phone string `orm:"size(20);null" json:"phone"`
Email string `orm:"size(100);null" json:"email"` Email string `orm:"size(100);null" json:"email"`
Status string `orm:"size(20);default(enabled)" json:"status"` // enabled, disabled Status string `orm:"size(20);default(enabled)" json:"status"`
AuditStatus string `orm:"size(20);default(pending)" json:"audit_status"` // pending, approved, rejected AuditStatus string `orm:"size(20);default(pending)" json:"audit_status"`
AuditComment string `orm:"type(text);null" json:"audit_comment"` AuditComment string `orm:"type(text);null" json:"audit_comment"`
AuditBy string `orm:"size(50);null" json:"audit_by"` AuditBy string `orm:"size(50);null" json:"audit_by"`
AuditTime *time.Time `orm:"null;type(datetime)" json:"audit_time"` AuditTime *time.Time `orm:"null;type(datetime)" json:"audit_time"`
Capacity int `orm:"default(0)" json:"capacity"`
CapacityUsed int `orm:"default(0)" json:"capacity_used"`
Remark string `orm:"type(text);null" json:"remark"` Remark string `orm:"type(text);null" json:"remark"`
CreateTime time.Time `orm:"auto_now_add;type(datetime)" json:"create_time"` CreateTime time.Time `orm:"auto_now_add;type(datetime)" json:"create_time"`
UpdateTime time.Time `orm:"auto_now;type(datetime)" json:"update_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 return err
} }
// DeleteTenant 软删除租户(设置 delete_time // DeleteTenant 软删除租户
func DeleteTenant(id int) error { func DeleteTenant(id int) error {
o := orm.NewOrm() o := orm.NewOrm()
deleteTime := time.Now() deleteTime := time.Now()
@ -77,7 +79,7 @@ func DeleteTenant(id int) error {
return err return err
} }
// AuditTenant 审核租户(只能审核未删除的) // AuditTenant 审核租户
func AuditTenant(id int, auditStatus, auditComment, auditBy string) error { func AuditTenant(id int, auditStatus, auditComment, auditBy string) error {
o := orm.NewOrm() o := orm.NewOrm()
tenant := Tenant{} tenant := Tenant{}
@ -99,7 +101,7 @@ func AuditTenant(id int, auditStatus, auditComment, auditBy string) error {
return err return err
} }
// GetTenantById 根据ID获取租户详情(只返回未删除的) // GetTenantById 根据ID获取租户详情
func GetTenantById(id int) (*Tenant, error) { func GetTenantById(id int) (*Tenant, error) {
o := orm.NewOrm() o := orm.NewOrm()
tenant := Tenant{} tenant := Tenant{}
@ -111,7 +113,7 @@ func GetTenantById(id int) (*Tenant, error) {
return &tenant, err return &tenant, err
} }
// GetTenantByName 根据名称获取租户详情(只返回未删除的) // GetTenantByName 根据名称获取租户详情
func GetTenantByName(name string) (*Tenant, error) { func GetTenantByName(name string) (*Tenant, error) {
o := orm.NewOrm() o := orm.NewOrm()
tenant := Tenant{} tenant := Tenant{}