增加文档管理功能
This commit is contained in:
@@ -0,0 +1,188 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
// OA 文档管理(文档库 / 文档图谱)
|
||||||
|
// 后端统一返回 { code, msg, data },响应拦截器只剥离外层,调用方需自行取 res.data
|
||||||
|
|
||||||
|
// ---------------- 分类 ----------------
|
||||||
|
|
||||||
|
// 分类树(每个节点带 doc_count)
|
||||||
|
export function getDocCategories() {
|
||||||
|
return request({
|
||||||
|
url: "/backend/oa/document/categories",
|
||||||
|
method: "get"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDocCategory(data) {
|
||||||
|
return request({
|
||||||
|
url: "/backend/oa/document/categories",
|
||||||
|
method: "post",
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateDocCategory(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/oa/document/categories/${id}`,
|
||||||
|
method: "post",
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDocCategory(id) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/oa/document/categories/${id}`,
|
||||||
|
method: "delete"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 文档 ----------------
|
||||||
|
|
||||||
|
export function getDocList(params) {
|
||||||
|
return request({
|
||||||
|
url: "/backend/oa/document/list",
|
||||||
|
method: "get",
|
||||||
|
params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDocStats() {
|
||||||
|
return request({
|
||||||
|
url: "/backend/oa/document/stats",
|
||||||
|
method: "get"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 详情(后端会累加查看次数)
|
||||||
|
export function getDocDetail(id) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/oa/document/detail/${id}`,
|
||||||
|
method: "get"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDoc(data) {
|
||||||
|
return request({
|
||||||
|
url: "/backend/oa/document/create",
|
||||||
|
method: "post",
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateDoc(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/oa/document/update/${id}`,
|
||||||
|
method: "post",
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDoc(id) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/oa/document/delete/${id}`,
|
||||||
|
method: "delete"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function batchDeleteDocs(ids) {
|
||||||
|
return request({
|
||||||
|
url: "/backend/oa/document/delete-batch",
|
||||||
|
method: "post",
|
||||||
|
data: { ids }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moveDocs(ids, categoryId) {
|
||||||
|
return request({
|
||||||
|
url: "/backend/oa/document/move",
|
||||||
|
method: "post",
|
||||||
|
data: { ids, category_id: categoryId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收藏 / 取消收藏
|
||||||
|
export function toggleDocStar(id) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/oa/document/star/${id}`,
|
||||||
|
method: "post"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录下载次数并取回下载地址
|
||||||
|
export function recordDocDownload(id) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/oa/document/download/${id}`,
|
||||||
|
method: "post"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 文档关联 ----------------
|
||||||
|
|
||||||
|
export function getDocLinks(params) {
|
||||||
|
return request({
|
||||||
|
url: "/backend/oa/document/links",
|
||||||
|
method: "get",
|
||||||
|
params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDocLink(data) {
|
||||||
|
return request({
|
||||||
|
url: "/backend/oa/document/links",
|
||||||
|
method: "post",
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDocLink(id) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/oa/document/links/${id}`,
|
||||||
|
method: "delete"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 文档私密共享 ----------------
|
||||||
|
|
||||||
|
// 租户用户列表(私密文档共享选人)
|
||||||
|
export function getDocMembers(params) {
|
||||||
|
return request({
|
||||||
|
url: "/backend/oa/document/members",
|
||||||
|
method: "get",
|
||||||
|
params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文档共享清单
|
||||||
|
export function getDocShares(id) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/oa/document/shares/${id}`,
|
||||||
|
method: "get"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全量设置文档共享(shares: [{ share_type, target_id }])
|
||||||
|
export function setDocShares(id, shares) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/oa/document/shares/${id}`,
|
||||||
|
method: "post",
|
||||||
|
data: { shares }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除单条共享
|
||||||
|
export function removeDocShare(sid) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/oa/document/share/${sid}`,
|
||||||
|
method: "delete"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 文档图谱 ----------------
|
||||||
|
|
||||||
|
export function getDocGraph(params) {
|
||||||
|
return request({
|
||||||
|
url: "/backend/oa/document/graph",
|
||||||
|
method: "get",
|
||||||
|
params
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,666 @@
|
|||||||
|
<template>
|
||||||
|
<div class="doc-graph-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<h2>文档图谱</h2>
|
||||||
|
<p>以知识图谱呈现文档之间的引用、相关与归属关系,点击节点查看文档详情</p>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button :icon="Refresh" @click="loadGraph">刷新</el-button>
|
||||||
|
<el-button type="primary" :icon="Aim" @click="fitView">重新布局</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="graph-layout">
|
||||||
|
<!-- 左侧:图谱 -->
|
||||||
|
<div class="graph-card">
|
||||||
|
<div class="filter-bar">
|
||||||
|
<el-input
|
||||||
|
v-model="filters.keyword"
|
||||||
|
clearable
|
||||||
|
placeholder="搜索文档"
|
||||||
|
style="width: 200px"
|
||||||
|
@keyup.enter="loadGraph"
|
||||||
|
/>
|
||||||
|
<el-tree-select
|
||||||
|
v-model="filters.category_id"
|
||||||
|
:data="categories"
|
||||||
|
:props="{ label: 'name', children: 'children' }"
|
||||||
|
node-key="id"
|
||||||
|
check-strictly
|
||||||
|
clearable
|
||||||
|
placeholder="全部分类"
|
||||||
|
style="width: 180px"
|
||||||
|
/>
|
||||||
|
<el-select
|
||||||
|
v-model="filters.relation"
|
||||||
|
clearable
|
||||||
|
placeholder="全部关系"
|
||||||
|
style="width: 130px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="r in DOC_RELATION_OPTIONS"
|
||||||
|
:key="r.value"
|
||||||
|
:label="r.label"
|
||||||
|
:value="r.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="filters.limit" style="width: 130px">
|
||||||
|
<el-option label="最多 100 个" :value="100" />
|
||||||
|
<el-option label="最多 300 个" :value="300" />
|
||||||
|
<el-option label="最多 600 个" :value="600" />
|
||||||
|
</el-select>
|
||||||
|
<el-checkbox v-model="filters.includeCategory">显示分类中心</el-checkbox>
|
||||||
|
<el-checkbox v-model="filters.onlyLinked">仅看有关系的文档</el-checkbox>
|
||||||
|
<el-button type="primary" :icon="Search" @click="loadGraph">生成图谱</el-button>
|
||||||
|
<el-button :icon="RefreshLeft" @click="resetFilters">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-loading="loading" class="chart-wrap">
|
||||||
|
<div ref="chartRef" class="chart" />
|
||||||
|
<el-empty
|
||||||
|
v-if="!loading && !nodes.length"
|
||||||
|
description="暂无可展示的文档,请先到文档库上传文档"
|
||||||
|
class="chart-empty"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<span class="legend-item">
|
||||||
|
<i class="dot" style="background: #409eff" />文档
|
||||||
|
</span>
|
||||||
|
<span class="legend-item">
|
||||||
|
<i class="dot" style="background: #e6a23c" />分类
|
||||||
|
</span>
|
||||||
|
<span class="legend-item">
|
||||||
|
<i class="line solid" />关联关系
|
||||||
|
</span>
|
||||||
|
<span class="legend-item">
|
||||||
|
<i class="line dashed" />归属分类
|
||||||
|
</span>
|
||||||
|
<span class="legend-tip">滚轮缩放 · 拖拽平移 · 点击节点查看详情</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:统计 -->
|
||||||
|
<div class="side-card">
|
||||||
|
<div class="side-title">图谱概览</div>
|
||||||
|
<div class="side-stats">
|
||||||
|
<div class="side-stat">
|
||||||
|
<span class="label">文档节点</span>
|
||||||
|
<span class="value">{{ stats.doc_count || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="side-stat">
|
||||||
|
<span class="label">分类节点</span>
|
||||||
|
<span class="value">{{ stats.category_count || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="side-stat">
|
||||||
|
<span class="label">关联关系</span>
|
||||||
|
<span class="value primary">{{ stats.link_count || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="side-stat">
|
||||||
|
<span class="label">孤立文档</span>
|
||||||
|
<span class="value warning">{{ stats.isolated_count || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="side-stat">
|
||||||
|
<span class="label">平均关联度</span>
|
||||||
|
<span class="value">{{
|
||||||
|
Number(stats.avg_degree || 0).toFixed(2)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider content-position="left">关系分布</el-divider>
|
||||||
|
<div v-if="relationCounts.length" class="relation-list">
|
||||||
|
<div v-for="r in relationCounts" :key="r.relation" class="relation-item">
|
||||||
|
<span>{{ r.label }}</span>
|
||||||
|
<el-progress
|
||||||
|
:percentage="relationPercent(r.count)"
|
||||||
|
:stroke-width="10"
|
||||||
|
:show-text="false"
|
||||||
|
style="width: 120px"
|
||||||
|
/>
|
||||||
|
<span class="relation-count">{{ r.count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="暂无关联关系" :image-size="60" />
|
||||||
|
|
||||||
|
<el-divider content-position="left">核心文档 Top 8</el-divider>
|
||||||
|
<div v-if="topNodes.length" class="top-list">
|
||||||
|
<div
|
||||||
|
v-for="(n, i) in topNodes"
|
||||||
|
:key="n.id"
|
||||||
|
class="top-item"
|
||||||
|
@click="openNode(n)"
|
||||||
|
>
|
||||||
|
<span class="rank" :class="{ top: i < 3 }">{{ i + 1 }}</span>
|
||||||
|
<span class="top-name" :title="n.name">{{ n.name }}</span>
|
||||||
|
<span class="top-degree">{{ n.degree }} 关联</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="暂无数据" :image-size="60" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DocDetailDrawer
|
||||||
|
v-model="detailVisible"
|
||||||
|
:doc-id="detailId"
|
||||||
|
:categories="categories"
|
||||||
|
@link-changed="loadGraph"
|
||||||
|
@star-changed="loadGraph"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { Aim, Refresh, RefreshLeft, Search } from "@element-plus/icons-vue";
|
||||||
|
import * as echarts from "echarts";
|
||||||
|
import DocDetailDrawer from "../documentlibrary/components/docDetailDrawer.vue";
|
||||||
|
import { getDocCategories, getDocGraph } from "@/api/oaDocument";
|
||||||
|
import { DOC_RELATION_OPTIONS, docTypeColor, docTypeLabel } from "../utils";
|
||||||
|
|
||||||
|
const chartRef = ref(null);
|
||||||
|
let chart = null;
|
||||||
|
let resizeObserver = null;
|
||||||
|
let themeObserver = null;
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const categories = ref([]);
|
||||||
|
const nodes = ref([]);
|
||||||
|
const links = ref([]);
|
||||||
|
const stats = ref({});
|
||||||
|
const relationCounts = ref([]);
|
||||||
|
const topNodes = ref([]);
|
||||||
|
|
||||||
|
const detailVisible = ref(false);
|
||||||
|
const detailId = ref(0);
|
||||||
|
|
||||||
|
const filters = reactive({
|
||||||
|
keyword: "",
|
||||||
|
category_id: null,
|
||||||
|
relation: "",
|
||||||
|
limit: 300,
|
||||||
|
includeCategory: true,
|
||||||
|
onlyLinked: false
|
||||||
|
});
|
||||||
|
|
||||||
|
function isDark() {
|
||||||
|
return document.documentElement.classList.contains("dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOption() {
|
||||||
|
const dark = isDark();
|
||||||
|
const textColor = dark ? "#e5eaf3" : "#303133";
|
||||||
|
const subTextColor = dark ? "#a3a6ad" : "#909399";
|
||||||
|
const axisColor = dark ? "#3a3a3a" : "#e0e6ed";
|
||||||
|
|
||||||
|
const data = nodes.value.map(n => {
|
||||||
|
const isCategory = n.category === 1;
|
||||||
|
const size = isCategory
|
||||||
|
? 42
|
||||||
|
: Math.min(58, 22 + (n.degree || 0) * 5);
|
||||||
|
return {
|
||||||
|
id: n.id,
|
||||||
|
name: n.name,
|
||||||
|
category: n.category,
|
||||||
|
symbolSize: size,
|
||||||
|
value: n.value,
|
||||||
|
// 供 tooltip / 点击回查
|
||||||
|
docId: n.doc_id,
|
||||||
|
categoryId: n.category_id,
|
||||||
|
docType: n.doc_type,
|
||||||
|
status: n.status,
|
||||||
|
ext: n.ext,
|
||||||
|
tags: n.tags,
|
||||||
|
ownerName: n.owner_name,
|
||||||
|
viewCount: n.view_count,
|
||||||
|
degree: n.degree,
|
||||||
|
itemStyle: {
|
||||||
|
color: isCategory ? "#e6a23c" : docTypeColor(n.doc_type),
|
||||||
|
borderColor: dark ? "#1f1f1f" : "#fff",
|
||||||
|
borderWidth: 2,
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowColor: dark ? "rgba(0,0,0,0.5)" : "rgba(0,0,0,0.15)"
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: "right",
|
||||||
|
fontSize: isCategory ? 13 : 12,
|
||||||
|
fontWeight: isCategory ? 600 : 400,
|
||||||
|
color: textColor
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const edgeList = links.value.map(l => ({
|
||||||
|
source: l.source,
|
||||||
|
target: l.target,
|
||||||
|
value: l.label,
|
||||||
|
lineStyle: {
|
||||||
|
width: l.kind === "belong" ? 1 : 1.6,
|
||||||
|
type: l.kind === "belong" ? "dashed" : "solid",
|
||||||
|
opacity: l.kind === "belong" ? 0.45 : 0.8,
|
||||||
|
curveness: 0.12
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
tooltip: {
|
||||||
|
trigger: "item",
|
||||||
|
backgroundColor: dark ? "#1f1f1f" : "#fff",
|
||||||
|
borderColor: axisColor,
|
||||||
|
textStyle: { color: textColor, fontSize: 12 },
|
||||||
|
formatter: params => {
|
||||||
|
if (params.dataType !== "node") {
|
||||||
|
return params.data?.value ? `关系:${params.data.value}` : "";
|
||||||
|
}
|
||||||
|
const d = params.data;
|
||||||
|
if (d.category === 1) {
|
||||||
|
return `<b>${d.name}</b><br/>分类中心 · ${d.degree || 0} 个关联`;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
`<b>${d.name}</b>`,
|
||||||
|
`类型:${docTypeLabel(d.docType)}`,
|
||||||
|
`标签:${d.tags || "-"}`,
|
||||||
|
`负责人:${d.ownerName || "-"}`,
|
||||||
|
`关联数:${d.degree || 0}`,
|
||||||
|
`查看:${d.viewCount || 0}`
|
||||||
|
].join("<br/>");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: "graph",
|
||||||
|
layout: "force",
|
||||||
|
roam: true,
|
||||||
|
draggable: true,
|
||||||
|
data,
|
||||||
|
links: edgeList,
|
||||||
|
categories: [
|
||||||
|
{ name: "文档", itemStyle: { color: "#409eff" } },
|
||||||
|
{ name: "分类", itemStyle: { color: "#e6a23c" } }
|
||||||
|
],
|
||||||
|
force: {
|
||||||
|
repulsion: nodes.value.length > 200 ? 90 : 160,
|
||||||
|
edgeLength: [70, 150],
|
||||||
|
gravity: 0.08
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
focus: "adjacency",
|
||||||
|
scale: 1.15,
|
||||||
|
lineStyle: { width: 3, opacity: 1 }
|
||||||
|
},
|
||||||
|
lineStyle: { color: "source", opacity: 0.8 },
|
||||||
|
label: { color: textColor },
|
||||||
|
selectedMode: "single"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
textStyle: { color: subTextColor }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChart() {
|
||||||
|
if (!chartRef.value) return;
|
||||||
|
if (!chart) {
|
||||||
|
chart = echarts.init(chartRef.value);
|
||||||
|
chart.on("click", handleChartClick);
|
||||||
|
}
|
||||||
|
chart.setOption(buildOption(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fitView() {
|
||||||
|
// 力导向图无内置"复位"动作,重新渲染即可回到初始布局与缩放
|
||||||
|
renderChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChartClick(params) {
|
||||||
|
if (params.dataType !== "node") return;
|
||||||
|
openNode(params.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNode(node) {
|
||||||
|
if (Number(node.category) === 1) {
|
||||||
|
// 分类节点:按该分类过滤图谱
|
||||||
|
filters.category_id = node.categoryId ?? node.category_id ?? null;
|
||||||
|
loadGraph();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const docId = node.docId ?? node.doc_id;
|
||||||
|
if (docId) {
|
||||||
|
detailId.value = docId;
|
||||||
|
detailVisible.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCategories() {
|
||||||
|
const res = await getDocCategories();
|
||||||
|
if (res?.code === 200) {
|
||||||
|
categories.value = res.data || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGraph() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getDocGraph({
|
||||||
|
keyword: filters.keyword || "",
|
||||||
|
category_id: filters.category_id || 0,
|
||||||
|
relation: filters.relation || "",
|
||||||
|
limit: filters.limit,
|
||||||
|
include_category: filters.includeCategory ? 1 : 0,
|
||||||
|
only_linked: filters.onlyLinked ? 1 : 0
|
||||||
|
});
|
||||||
|
if (res?.code === 200) {
|
||||||
|
const data = res.data || {};
|
||||||
|
nodes.value = data.nodes || [];
|
||||||
|
links.value = data.links || [];
|
||||||
|
stats.value = data.stats || {};
|
||||||
|
relationCounts.value = stats.value.relation_counts || [];
|
||||||
|
topNodes.value = stats.value.top_nodes || [];
|
||||||
|
await nextTick();
|
||||||
|
renderChart();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.msg || "加载图谱失败");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
filters.keyword = "";
|
||||||
|
filters.category_id = null;
|
||||||
|
filters.relation = "";
|
||||||
|
filters.limit = 300;
|
||||||
|
filters.includeCategory = true;
|
||||||
|
filters.onlyLinked = false;
|
||||||
|
loadGraph();
|
||||||
|
}
|
||||||
|
|
||||||
|
const relationMax = computed(() =>
|
||||||
|
Math.max(1, ...relationCounts.value.map(r => r.count || 0))
|
||||||
|
);
|
||||||
|
function relationPercent(count) {
|
||||||
|
return Math.round(((count || 0) * 100) / relationMax.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
chart?.resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadCategories();
|
||||||
|
loadGraph();
|
||||||
|
nextTick(() => {
|
||||||
|
if (!chartRef.value) return;
|
||||||
|
resizeObserver = new ResizeObserver(handleResize);
|
||||||
|
resizeObserver.observe(chartRef.value);
|
||||||
|
});
|
||||||
|
themeObserver = new MutationObserver(() => renderChart());
|
||||||
|
themeObserver.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class"]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
themeObserver?.disconnect();
|
||||||
|
chart?.dispose();
|
||||||
|
chart = null;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.doc-graph-page {
|
||||||
|
padding: 16px 20px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header p {
|
||||||
|
margin: 0;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-card {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 620px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-empty {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
width: 22px;
|
||||||
|
height: 0;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&.solid {
|
||||||
|
border-top: 2px solid #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dashed {
|
||||||
|
border-top: 2px dashed #c0c4cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-tip {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-card {
|
||||||
|
width: 300px;
|
||||||
|
flex: 0 0 300px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-stat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-stat .label {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-stat .value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
&.warning {
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.relation-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relation-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relation-count {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 5px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&.top {
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-degree {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 暗色主题适配 =====
|
||||||
|
html.dark & {
|
||||||
|
.page-header p {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-card,
|
||||||
|
.side-card {
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border-color: var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend,
|
||||||
|
.side-stat .label,
|
||||||
|
.relation-count,
|
||||||
|
.top-degree,
|
||||||
|
.legend-tip {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-stat .value {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-item:hover {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,563 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer
|
||||||
|
v-model="visible"
|
||||||
|
:title="detail?.title || '文档详情'"
|
||||||
|
size="560px"
|
||||||
|
@closed="handleClosed"
|
||||||
|
>
|
||||||
|
<div v-loading="loading" class="doc-detail">
|
||||||
|
<template v-if="detail">
|
||||||
|
<div class="detail-tags">
|
||||||
|
<el-tag :color="docTypeColor(detail.doc_type)" effect="dark" size="small">
|
||||||
|
{{ docTypeLabel(detail.doc_type) }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag :type="docStatusTagType(detail.status)" size="small">
|
||||||
|
{{ docStatusLabel(detail.status) }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-if="detail.is_star === 1" type="warning" size="small" effect="plain">
|
||||||
|
已收藏
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-if="detail.visibility === 1"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
effect="plain"
|
||||||
|
>
|
||||||
|
私密
|
||||||
|
</el-tag>
|
||||||
|
<el-tag type="info" size="small" effect="plain">
|
||||||
|
v{{ detail.version || 1 }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-descriptions :column="2" border size="small" class="detail-desc">
|
||||||
|
<el-descriptions-item label="所属分类">
|
||||||
|
{{ categoryName(detail.category_id) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="负责人">
|
||||||
|
{{ detail.owner_name || "-" }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建人">
|
||||||
|
{{ detail.creator_name || "-" }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="文件大小">
|
||||||
|
{{ formatFileSize(detail.size) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="查看 / 下载">
|
||||||
|
{{ detail.view_count || 0 }} / {{ detail.download_count || 0 }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">
|
||||||
|
{{ formatDateTime(detail.update_time) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="原始文件名" :span="2">
|
||||||
|
{{ detail.file_name || "-" }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="标签" :span="2">
|
||||||
|
<el-tag
|
||||||
|
v-for="t in parseTags(detail.tags)"
|
||||||
|
:key="t"
|
||||||
|
size="small"
|
||||||
|
effect="plain"
|
||||||
|
class="tag-item"
|
||||||
|
>{{ t }}</el-tag
|
||||||
|
>
|
||||||
|
<span v-if="!parseTags(detail.tags).length">-</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="摘要" :span="2">
|
||||||
|
{{ detail.summary || "-" }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div class="detail-actions">
|
||||||
|
<el-button type="primary" :icon="View" @click="emit('preview', detail)"
|
||||||
|
>预览</el-button
|
||||||
|
>
|
||||||
|
<el-button :icon="Download" @click="emit('download', detail)"
|
||||||
|
>下载</el-button
|
||||||
|
>
|
||||||
|
<el-button :icon="Star" @click="handleStar">{{
|
||||||
|
detail.is_star === 1 ? "取消收藏" : "收藏"
|
||||||
|
}}</el-button>
|
||||||
|
<el-button :icon="Edit" @click="emit('edit', detail)">编辑</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider content-position="left">共享范围(私密文档)</el-divider>
|
||||||
|
|
||||||
|
<div v-if="detail.visibility === 1" class="share-panel">
|
||||||
|
<div class="share-list">
|
||||||
|
<el-tag
|
||||||
|
v-for="s in shares"
|
||||||
|
:key="s.id"
|
||||||
|
type="info"
|
||||||
|
effect="plain"
|
||||||
|
closable
|
||||||
|
@close="removeShare(s)"
|
||||||
|
>
|
||||||
|
{{ s.share_type === 1 ? "部门" : "用户" }}:{{
|
||||||
|
s.target_name || s.target_id
|
||||||
|
}}
|
||||||
|
</el-tag>
|
||||||
|
<span v-if="!shares.length" class="empty-tip">仅创建者本人可见</span>
|
||||||
|
</div>
|
||||||
|
<div class="share-add">
|
||||||
|
<el-select v-model="shareForm.type" style="width: 110px">
|
||||||
|
<el-option :value="0" label="用户" />
|
||||||
|
<el-option :value="1" label="部门" />
|
||||||
|
</el-select>
|
||||||
|
<el-select
|
||||||
|
v-if="shareForm.type === 0"
|
||||||
|
v-model="shareForm.target"
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
reserve-keyword
|
||||||
|
:remote-method="searchMembers"
|
||||||
|
:loading="searchingMember"
|
||||||
|
placeholder="搜索用户"
|
||||||
|
style="flex: 1"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="m in memberOptions"
|
||||||
|
:key="m.uid"
|
||||||
|
:label="`${m.name || m.account}(${m.account})`"
|
||||||
|
:value="m.uid"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-tree-select
|
||||||
|
v-else
|
||||||
|
v-model="shareForm.target"
|
||||||
|
:data="orgTree"
|
||||||
|
:props="{ label: 'label', value: 'value', children: 'children' }"
|
||||||
|
value-key="value"
|
||||||
|
check-strictly
|
||||||
|
clearable
|
||||||
|
placeholder="选择部门"
|
||||||
|
style="flex: 1"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!shareForm.target"
|
||||||
|
@click="addShare"
|
||||||
|
>添加</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="share-public-tip">
|
||||||
|
该文档为「租户公开」,本租户内所有成员均可访问。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider content-position="left">关联文档</el-divider>
|
||||||
|
|
||||||
|
<div class="link-add">
|
||||||
|
<el-select
|
||||||
|
v-model="linkForm.target_id"
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
clearable
|
||||||
|
reserve-keyword
|
||||||
|
placeholder="搜索要关联的文档"
|
||||||
|
:remote-method="searchDocs"
|
||||||
|
:loading="searching"
|
||||||
|
style="flex: 1"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="d in docOptions"
|
||||||
|
:key="d.id"
|
||||||
|
:label="d.title"
|
||||||
|
:value="d.id"
|
||||||
|
:disabled="d.id === detail.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="linkForm.relation" style="width: 110px">
|
||||||
|
<el-option
|
||||||
|
v-for="r in DOC_RELATION_OPTIONS"
|
||||||
|
:key="r.value"
|
||||||
|
:label="r.label"
|
||||||
|
:value="r.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-button type="primary" :icon="Connection" @click="handleAddLink"
|
||||||
|
>关联</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="links" size="small" class="link-table">
|
||||||
|
<el-table-column label="关系" width="90">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small" effect="plain">{{ row.relation_cn }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="文档" min-width="180" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="link-title" @click="openPeer(row)">{{ peer(row).title }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="remark" label="说明" min-width="100" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="70" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="danger" @click="handleDeleteLink(row)"
|
||||||
|
>解除</el-button
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template #empty>
|
||||||
|
<el-empty description="暂无关联文档" :image-size="60" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-empty v-else-if="!loading" description="未选择文档" />
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
import {
|
||||||
|
Connection,
|
||||||
|
Download,
|
||||||
|
Edit,
|
||||||
|
Star,
|
||||||
|
View
|
||||||
|
} from "@element-plus/icons-vue";
|
||||||
|
import {
|
||||||
|
createDocLink,
|
||||||
|
deleteDocLink,
|
||||||
|
getDocDetail,
|
||||||
|
getDocLinks,
|
||||||
|
getDocList,
|
||||||
|
getDocMembers,
|
||||||
|
getDocShares,
|
||||||
|
removeDocShare,
|
||||||
|
setDocShares,
|
||||||
|
toggleDocStar
|
||||||
|
} from "@/api/oaDocument";
|
||||||
|
import { oaOrganizationApi } from "@/api/organization";
|
||||||
|
import {
|
||||||
|
DOC_RELATION_OPTIONS,
|
||||||
|
docStatusLabel,
|
||||||
|
docStatusTagType,
|
||||||
|
docTypeColor,
|
||||||
|
docTypeLabel,
|
||||||
|
findCategory,
|
||||||
|
formatDateTime,
|
||||||
|
formatFileSize,
|
||||||
|
parseTags
|
||||||
|
} from "../../utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
docId: { type: [Number, String], default: 0 },
|
||||||
|
categories: { type: Array, default: () => [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
"update:modelValue",
|
||||||
|
"preview",
|
||||||
|
"download",
|
||||||
|
"edit",
|
||||||
|
"star-changed",
|
||||||
|
"open-doc",
|
||||||
|
"link-changed"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const visible = ref(props.modelValue);
|
||||||
|
const loading = ref(false);
|
||||||
|
const detail = ref(null);
|
||||||
|
const links = ref([]);
|
||||||
|
|
||||||
|
const searching = ref(false);
|
||||||
|
const docOptions = ref([]);
|
||||||
|
const linkForm = ref({ target_id: null, relation: "related" });
|
||||||
|
|
||||||
|
// 共享范围
|
||||||
|
const shares = ref([]);
|
||||||
|
const shareForm = ref({ type: 0, target: null });
|
||||||
|
const memberOptions = ref([]);
|
||||||
|
const searchingMember = ref(false);
|
||||||
|
const orgTree = ref([]);
|
||||||
|
|
||||||
|
// 组织机构树(部门选择用),平铺结构转树
|
||||||
|
const buildOrgTree = (list) => {
|
||||||
|
const nodes = (list || []).map((o) => ({
|
||||||
|
value: Number(o.id),
|
||||||
|
label: o.org_name || o.name || `部门${o.id}`,
|
||||||
|
parent_id: Number(o.parent_id || 0)
|
||||||
|
}));
|
||||||
|
const map = new Map();
|
||||||
|
nodes.forEach((n) => map.set(n.value, { ...n, children: [] }));
|
||||||
|
const tree = [];
|
||||||
|
map.forEach((node) => {
|
||||||
|
const parent = map.get(node.parent_id);
|
||||||
|
if (parent) parent.children.push(node);
|
||||||
|
else tree.push(node);
|
||||||
|
});
|
||||||
|
return tree;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadOrgTree = async () => {
|
||||||
|
try {
|
||||||
|
const res = await oaOrganizationApi.getOrganizationTree();
|
||||||
|
if (res?.code === 200) {
|
||||||
|
const list = res?.data?.list || res?.data || [];
|
||||||
|
orgTree.value = buildOrgTree(list);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
orgTree.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadShares = async (docId) => {
|
||||||
|
const res = await getDocShares(docId);
|
||||||
|
shares.value = res?.code === 200 ? res.data || [] : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
async function searchMembers(keyword) {
|
||||||
|
searchingMember.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getDocMembers({ keyword, limit: 50 });
|
||||||
|
memberOptions.value = res?.code === 200 ? res.data || [] : [];
|
||||||
|
} finally {
|
||||||
|
searchingMember.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addShare() {
|
||||||
|
if (!shareForm.value.target) return;
|
||||||
|
// 全量替换:保留现有共享 + 新增一条
|
||||||
|
const items = shares.value.map((s) => ({
|
||||||
|
share_type: s.share_type,
|
||||||
|
target_id: s.target_id
|
||||||
|
}));
|
||||||
|
items.push({ share_type: shareForm.value.type, target_id: shareForm.value.target });
|
||||||
|
const res = await setDocShares(detail.value.id, items);
|
||||||
|
if (res?.code === 200) {
|
||||||
|
shares.value = res.data || [];
|
||||||
|
shareForm.value.target = null;
|
||||||
|
ElMessage.success("已添加共享");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.msg || "操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeShare(s) {
|
||||||
|
const res = await removeDocShare(s.id);
|
||||||
|
if (res?.code === 200) {
|
||||||
|
shares.value = shares.value.filter((x) => x.id !== s.id);
|
||||||
|
ElMessage.success("已移除共享");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.msg || "操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
val => {
|
||||||
|
visible.value = val;
|
||||||
|
if (val && props.docId) {
|
||||||
|
loadDetail(props.docId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(visible, val => emit("update:modelValue", val));
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.docId,
|
||||||
|
id => {
|
||||||
|
if (visible.value && id) {
|
||||||
|
loadDetail(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function loadDetail(id) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const [detailRes, linkRes] = await Promise.all([
|
||||||
|
getDocDetail(id),
|
||||||
|
getDocLinks({ doc_id: id })
|
||||||
|
]);
|
||||||
|
if (detailRes?.code === 200) {
|
||||||
|
detail.value = detailRes.data;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(detailRes?.msg || "加载文档失败");
|
||||||
|
}
|
||||||
|
links.value = linkRes?.code === 200 ? linkRes.data || [] : [];
|
||||||
|
loadShares(id);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function categoryName(id) {
|
||||||
|
if (!id) return "未分类";
|
||||||
|
return findCategory(props.categories, id)?.name || "未分类";
|
||||||
|
}
|
||||||
|
|
||||||
|
function peer(row) {
|
||||||
|
if (!detail.value) return { id: 0, title: "-" };
|
||||||
|
if (row.source_id === detail.value.id) {
|
||||||
|
return { id: row.target_id, title: row.target_title || `文档#${row.target_id}` };
|
||||||
|
}
|
||||||
|
return { id: row.source_id, title: row.source_title || `文档#${row.source_id}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPeer(row) {
|
||||||
|
const p = peer(row);
|
||||||
|
if (p.id) emit("open-doc", p.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchDocs(keyword) {
|
||||||
|
searching.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getDocList({ keyword, pageSize: 20, page: 1 });
|
||||||
|
docOptions.value = res?.code === 200 ? res.data?.list || [] : [];
|
||||||
|
} finally {
|
||||||
|
searching.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAddLink() {
|
||||||
|
if (!linkForm.value.target_id) {
|
||||||
|
ElMessage.warning("请选择要关联的文档");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await createDocLink({
|
||||||
|
source_id: detail.value.id,
|
||||||
|
target_id: linkForm.value.target_id,
|
||||||
|
relation: linkForm.value.relation,
|
||||||
|
remark: ""
|
||||||
|
});
|
||||||
|
if (res?.code === 200) {
|
||||||
|
ElMessage.success("已建立关联");
|
||||||
|
linkForm.value.target_id = null;
|
||||||
|
await refreshLinks();
|
||||||
|
emit("link-changed");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.msg || "关联失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshLinks() {
|
||||||
|
if (!detail.value) return;
|
||||||
|
const res = await getDocLinks({ doc_id: detail.value.id });
|
||||||
|
links.value = res?.code === 200 ? res.data || [] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteLink(row) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm("确定解除该关联关系吗?", "提示", {
|
||||||
|
type: "warning",
|
||||||
|
confirmButtonText: "解除",
|
||||||
|
cancelButtonText: "取消"
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await deleteDocLink(row.id);
|
||||||
|
if (res?.code === 200) {
|
||||||
|
ElMessage.success("已解除关联");
|
||||||
|
await refreshLinks();
|
||||||
|
emit("link-changed");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.msg || "操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStar() {
|
||||||
|
const res = await toggleDocStar(detail.value.id);
|
||||||
|
if (res?.code === 200) {
|
||||||
|
detail.value.is_star = res.data?.is_star;
|
||||||
|
ElMessage.success(res.data?.is_star === 1 ? "已收藏" : "已取消收藏");
|
||||||
|
emit("star-changed");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.msg || "操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClosed() {
|
||||||
|
detail.value = null;
|
||||||
|
links.value = [];
|
||||||
|
docOptions.value = [];
|
||||||
|
linkForm.value = { target_id: null, relation: "related" };
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ reload: () => detail.value && loadDetail(detail.value.id) });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.doc-detail {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-tags {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-desc {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-item {
|
||||||
|
margin-right: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-add {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-panel {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-list .empty-tip {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-add {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-public-tip {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-title {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__label) {
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" title="编辑文档" width="560px" @closed="handleClosed">
|
||||||
|
<el-form :model="form" label-width="90px">
|
||||||
|
<el-form-item label="文档标题" required>
|
||||||
|
<el-input v-model="form.title" maxlength="120" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="所属分类">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="form.category_id"
|
||||||
|
:data="categories"
|
||||||
|
:props="{ label: 'name', children: 'children' }"
|
||||||
|
node-key="id"
|
||||||
|
check-strictly
|
||||||
|
clearable
|
||||||
|
placeholder="未分类"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标签">
|
||||||
|
<el-select
|
||||||
|
v-model="form.tagList"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option v-for="t in form.tagList" :key="t" :label="t" :value="t" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio :value="0">草稿</el-radio>
|
||||||
|
<el-radio :value="1">已发布</el-radio>
|
||||||
|
<el-radio :value="2">已归档</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="可见性">
|
||||||
|
<el-radio-group v-model="form.visibility">
|
||||||
|
<el-radio :value="0">租户公开</el-radio>
|
||||||
|
<el-radio :value="1">私密</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
私密文档仅本人及被共享的用户/部门可见;设为私密后可在详情中设置共享范围。
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="版本号">
|
||||||
|
<el-input-number v-model="form.version" :min="1" :max="999" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="负责人">
|
||||||
|
<el-input v-model="form.owner_name" maxlength="50" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="摘要">
|
||||||
|
<el-input
|
||||||
|
v-model="form.summary"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSubmit"
|
||||||
|
>保存</el-button
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { updateDoc } from "@/api/oaDocument";
|
||||||
|
import { parseTags } from "../../utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
doc: { type: Object, default: null },
|
||||||
|
categories: { type: Array, default: () => [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "saved"]);
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const submitting = ref(false);
|
||||||
|
const form = ref({
|
||||||
|
title: "",
|
||||||
|
category_id: 0,
|
||||||
|
tagList: [],
|
||||||
|
status: 0,
|
||||||
|
visibility: 0,
|
||||||
|
version: 1,
|
||||||
|
owner_name: "",
|
||||||
|
summary: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
val => {
|
||||||
|
visible.value = val;
|
||||||
|
if (val && props.doc) {
|
||||||
|
form.value = {
|
||||||
|
title: props.doc.title || "",
|
||||||
|
category_id: props.doc.category_id || 0,
|
||||||
|
tagList: parseTags(props.doc.tags),
|
||||||
|
status: props.doc.status ?? 0,
|
||||||
|
visibility: props.doc.visibility ?? 0,
|
||||||
|
version: props.doc.version || 1,
|
||||||
|
owner_name: props.doc.owner_name || "",
|
||||||
|
summary: props.doc.summary || ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(visible, val => emit("update:modelValue", val));
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!form.value.title?.trim()) {
|
||||||
|
ElMessage.warning("请输入文档标题");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
const res = await updateDoc(props.doc.id, {
|
||||||
|
title: form.value.title.trim(),
|
||||||
|
category_id: form.value.category_id || 0,
|
||||||
|
tags: form.value.tagList.join(","),
|
||||||
|
status: form.value.status,
|
||||||
|
visibility: form.value.visibility || 0,
|
||||||
|
version: form.value.version,
|
||||||
|
owner_name: form.value.owner_name,
|
||||||
|
summary: form.value.summary
|
||||||
|
});
|
||||||
|
if (res?.code === 200) {
|
||||||
|
ElMessage.success("保存成功");
|
||||||
|
visible.value = false;
|
||||||
|
emit("saved");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.msg || "保存失败");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClosed() {
|
||||||
|
form.value = {
|
||||||
|
title: "",
|
||||||
|
category_id: 0,
|
||||||
|
tagList: [],
|
||||||
|
status: 0,
|
||||||
|
visibility: 0,
|
||||||
|
version: 1,
|
||||||
|
owner_name: "",
|
||||||
|
summary: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
title="上传文档"
|
||||||
|
width="620px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@closed="handleClosed"
|
||||||
|
>
|
||||||
|
<el-form :model="form" label-width="90px">
|
||||||
|
<el-form-item label="文件" required>
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
drag
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="1"
|
||||||
|
:file-list="fileList"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:on-remove="handleFileRemove"
|
||||||
|
:on-exceed="handleExceed"
|
||||||
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.md,.csv,.zip,.rar,.7z,.jpg,.jpeg,.png,.gif,.webp,.mp4,.mp3"
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||||
|
<div class="el-upload__text">将文件拖到此处,或<em>点击选择</em></div>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
支持 PDF / Office / 图片 / 压缩包等,单个文件不超过 200MB
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
<el-progress
|
||||||
|
v-if="uploading"
|
||||||
|
:percentage="uploadPercent"
|
||||||
|
:stroke-width="14"
|
||||||
|
striped
|
||||||
|
style="margin-top: 10px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="文档标题">
|
||||||
|
<el-input
|
||||||
|
v-model="form.title"
|
||||||
|
maxlength="120"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="留空则使用文件名"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="所属分类">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="form.category_id"
|
||||||
|
:data="categoryTree"
|
||||||
|
:props="treeProps"
|
||||||
|
node-key="id"
|
||||||
|
check-strictly
|
||||||
|
clearable
|
||||||
|
placeholder="未分类"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="标签">
|
||||||
|
<el-select
|
||||||
|
v-model="form.tagList"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
placeholder="输入后回车可新建标签"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option v-for="t in presetTags" :key="t" :label="t" :value="t" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio :value="0">草稿</el-radio>
|
||||||
|
<el-radio :value="1">已发布</el-radio>
|
||||||
|
<el-radio :value="2">已归档</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="可见性">
|
||||||
|
<el-radio-group v-model="form.visibility">
|
||||||
|
<el-radio :value="0">租户公开</el-radio>
|
||||||
|
<el-radio :value="1">私密</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
私密文档仅本人及被共享的用户/部门可见;设为私密后可在详情中设置共享范围。
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="负责人">
|
||||||
|
<el-input
|
||||||
|
v-model="form.owner_name"
|
||||||
|
maxlength="50"
|
||||||
|
placeholder="选填,默认当前操作人"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="摘要">
|
||||||
|
<el-input
|
||||||
|
v-model="form.summary"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="选填,简要描述文档内容"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="submitting"
|
||||||
|
:disabled="uploading"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>保存</el-button
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { UploadFilled } from "@element-plus/icons-vue";
|
||||||
|
import { uploadFile } from "@/api/file";
|
||||||
|
import { createDoc } from "@/api/oaDocument";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
categories: { type: Array, default: () => [] },
|
||||||
|
defaultCategoryId: { type: Number, default: 0 },
|
||||||
|
currentUserName: { type: String, default: "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "saved"]);
|
||||||
|
|
||||||
|
const treeProps = { label: "name", children: "children" };
|
||||||
|
const presetTags = ["制度", "流程", "模板", "合同", "培训", "报表", "归档"];
|
||||||
|
|
||||||
|
const visible = ref(props.modelValue);
|
||||||
|
const uploadRef = ref(null);
|
||||||
|
const fileList = ref([]);
|
||||||
|
const rawFile = ref(null);
|
||||||
|
const uploading = ref(false);
|
||||||
|
const uploadPercent = ref(0);
|
||||||
|
const submitting = ref(false);
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
title: "",
|
||||||
|
category_id: 0,
|
||||||
|
tagList: [],
|
||||||
|
status: 1,
|
||||||
|
visibility: 0,
|
||||||
|
owner_name: "",
|
||||||
|
summary: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const categoryTree = computed(() => props.categories || []);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
val => {
|
||||||
|
visible.value = val;
|
||||||
|
if (val) {
|
||||||
|
form.value = {
|
||||||
|
title: "",
|
||||||
|
category_id: props.defaultCategoryId || 0,
|
||||||
|
tagList: [],
|
||||||
|
status: 1,
|
||||||
|
visibility: 0,
|
||||||
|
owner_name: props.currentUserName || "",
|
||||||
|
summary: ""
|
||||||
|
};
|
||||||
|
uploadPercent.value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(visible, val => emit("update:modelValue", val));
|
||||||
|
|
||||||
|
function handleFileChange(file) {
|
||||||
|
rawFile.value = file.raw || file;
|
||||||
|
fileList.value = [file];
|
||||||
|
if (!form.value.title) {
|
||||||
|
form.value.title = String(file.name || "").replace(/\.[^.]+$/, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileRemove() {
|
||||||
|
rawFile.value = null;
|
||||||
|
fileList.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExceed() {
|
||||||
|
ElMessage.warning("单次只能上传 1 个文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFormData() {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("file", rawFile.value);
|
||||||
|
fd.append("cate", "0");
|
||||||
|
fd.append("scope", "tenant");
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!rawFile.value) {
|
||||||
|
ElMessage.warning("请选择要上传的文件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rawFile.value.size > 200 * 1024 * 1024) {
|
||||||
|
ElMessage.warning("单个文件不能超过 200MB");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
uploading.value = true;
|
||||||
|
uploadPercent.value = 0;
|
||||||
|
try {
|
||||||
|
const upRes = await uploadFile(buildFormData(), {
|
||||||
|
onUploadProgress: e => {
|
||||||
|
if (!e?.total) return;
|
||||||
|
uploadPercent.value = Math.min(
|
||||||
|
99,
|
||||||
|
Math.round((e.loaded * 100) / e.total)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 秒传命中时返回 code=201,同样携带 url/id/name
|
||||||
|
const upData = upRes?.data || {};
|
||||||
|
if (upRes?.code !== 200 && upRes?.code !== 201) {
|
||||||
|
ElMessage.error(upRes?.msg || "文件上传失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uploadPercent.value = 100;
|
||||||
|
|
||||||
|
const name = upData.name || rawFile.value.name || "";
|
||||||
|
const ext = name.includes(".") ? name.split(".").pop() : "";
|
||||||
|
const res = await createDoc({
|
||||||
|
title: form.value.title?.trim() || name.replace(/\.[^.]+$/, ""),
|
||||||
|
category_id: form.value.category_id || 0,
|
||||||
|
file_id: upData.id || 0,
|
||||||
|
file_url: upData.url || "",
|
||||||
|
file_name: name,
|
||||||
|
ext,
|
||||||
|
size: rawFile.value.size || 0,
|
||||||
|
tags: form.value.tagList.join(","),
|
||||||
|
summary: form.value.summary || "",
|
||||||
|
status: form.value.status,
|
||||||
|
visibility: form.value.visibility || 0,
|
||||||
|
owner_name: form.value.owner_name || ""
|
||||||
|
});
|
||||||
|
if (res?.code === 200) {
|
||||||
|
ElMessage.success("上传成功");
|
||||||
|
visible.value = false;
|
||||||
|
emit("saved");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.msg || "保存失败");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error(err?.message || "上传失败");
|
||||||
|
} finally {
|
||||||
|
uploading.value = false;
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClosed() {
|
||||||
|
rawFile.value = null;
|
||||||
|
fileList.value = [];
|
||||||
|
uploadPercent.value = 0;
|
||||||
|
uploadRef.value?.clearFiles?.();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
:deep(.el-upload-dragger) {
|
||||||
|
padding: 18px 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,143 @@
|
|||||||
|
// OA 文档管理(文档库 / 文档图谱)公共工具
|
||||||
|
// doc_type: 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频
|
||||||
|
// status : 0-草稿 1-已发布 2-已归档
|
||||||
|
// relation: reference-引用 related-相关 version-版本 belong-从属
|
||||||
|
|
||||||
|
export const DOC_TYPE_OPTIONS = [
|
||||||
|
{ value: 1, label: "文档", color: "#409eff" },
|
||||||
|
{ value: 2, label: "表格", color: "#67c23a" },
|
||||||
|
{ value: 3, label: "演示", color: "#e6a23c" },
|
||||||
|
{ value: 4, label: "PDF", color: "#f56c6c" },
|
||||||
|
{ value: 5, label: "图片", color: "#909399" },
|
||||||
|
{ value: 6, label: "压缩包", color: "#a06bd6" },
|
||||||
|
{ value: 7, label: "视频", color: "#00b8d9" },
|
||||||
|
{ value: 8, label: "音频", color: "#ff8c42" },
|
||||||
|
{ value: 0, label: "其他", color: "#8d8d8d" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DOC_STATUS_OPTIONS = [
|
||||||
|
{ value: 0, label: "草稿", type: "warning" },
|
||||||
|
{ value: 1, label: "已发布", type: "success" },
|
||||||
|
{ value: 2, label: "已归档", type: "info" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DOC_RELATION_OPTIONS = [
|
||||||
|
{ value: "reference", label: "引用" },
|
||||||
|
{ value: "related", label: "相关" },
|
||||||
|
{ value: "version", label: "版本" },
|
||||||
|
{ value: "belong", label: "从属" }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 可见性:0-租户公开 1-私密
|
||||||
|
export const DOC_VISIBILITY_OPTIONS = [
|
||||||
|
{ value: 0, label: "租户公开" },
|
||||||
|
{ value: 1, label: "私密" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export function docVisibilityLabel(visibility) {
|
||||||
|
return Number(visibility) === 1 ? "私密" : "公开";
|
||||||
|
}
|
||||||
|
|
||||||
|
const docTypeMap = new Map(DOC_TYPE_OPTIONS.map(i => [i.value, i]));
|
||||||
|
const docStatusMap = new Map(DOC_STATUS_OPTIONS.map(i => [i.value, i]));
|
||||||
|
const relationMap = new Map(DOC_RELATION_OPTIONS.map(i => [i.value, i.label]));
|
||||||
|
|
||||||
|
export function docTypeLabel(docType) {
|
||||||
|
return docTypeMap.get(Number(docType))?.label || "其他";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function docTypeColor(docType) {
|
||||||
|
return docTypeMap.get(Number(docType))?.color || "#8d8d8d";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function docStatusLabel(status) {
|
||||||
|
return docStatusMap.get(Number(status))?.label || "草稿";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function docStatusTagType(status) {
|
||||||
|
return docStatusMap.get(Number(status))?.type || "warning";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function relationLabel(relation) {
|
||||||
|
return relationMap.get(relation) || relation || "相关";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼接文件访问地址:云存储返回完整 http(s) 地址时直接使用,
|
||||||
|
* 本地存储返回相对路径时拼接接口基址。
|
||||||
|
*/
|
||||||
|
export function getFileUrl(url) {
|
||||||
|
if (!url) return "";
|
||||||
|
const normalized = String(url).trim().replace(/\\/g, "/");
|
||||||
|
if (/^(https?:\/\/|data:|blob:)/i.test(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
const baseUrl = String(import.meta.env.VITE_API_BASE_URL || "").replace(/\/$/, "");
|
||||||
|
const path = normalized.startsWith("/") ? normalized : `/${normalized}`;
|
||||||
|
return `${baseUrl}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否为可内联预览的图片 */
|
||||||
|
export function isImageDoc(row) {
|
||||||
|
return ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(
|
||||||
|
String(row?.ext || "").toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否为可在浏览器直接打开的 PDF */
|
||||||
|
export function isPdfDoc(row) {
|
||||||
|
return String(row?.ext || "").toLowerCase() === "pdf";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFileSize(size) {
|
||||||
|
const bytes = Number(size || 0);
|
||||||
|
if (!bytes) return "-";
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||||
|
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDateTime(value) {
|
||||||
|
if (!value) return "-";
|
||||||
|
const d = new Date(value);
|
||||||
|
if (Number.isNaN(d.getTime())) return String(value);
|
||||||
|
const pad = n => String(n).padStart(2, "0");
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(
|
||||||
|
d.getHours()
|
||||||
|
)}:${pad(d.getMinutes())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 后端返回的 tags 为逗号分隔字符串,统一转为数组 */
|
||||||
|
export function parseTags(tags) {
|
||||||
|
if (Array.isArray(tags)) return tags.filter(Boolean);
|
||||||
|
return String(tags || "")
|
||||||
|
.split(/[,,;;]/)
|
||||||
|
.map(t => t.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扁平化分类树,用于 tree-select / 下拉选择。
|
||||||
|
* @param {Array} tree 后端返回的分类树
|
||||||
|
* @param {number} depth 起始层级
|
||||||
|
*/
|
||||||
|
export function flattenCategories(tree, depth = 0, out = []) {
|
||||||
|
(tree || []).forEach(node => {
|
||||||
|
out.push({ id: node.id, name: node.name, depth, doc_count: node.doc_count });
|
||||||
|
if (node.children?.length) {
|
||||||
|
flattenCategories(node.children, depth + 1, out);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从分类树中查找节点 */
|
||||||
|
export function findCategory(tree, id) {
|
||||||
|
for (const node of tree || []) {
|
||||||
|
if (node.id === id) return node;
|
||||||
|
const hit = findCategory(node.children, id);
|
||||||
|
if (hit) return hit;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,657 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"server/models"
|
||||||
|
"server/pkg/jwtutil"
|
||||||
|
"server/services"
|
||||||
|
|
||||||
|
beego "github.com/beego/beego/v2/server/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 控制器层专用错误,仅用于响应转换
|
||||||
|
var (
|
||||||
|
errOaDocUnauthorized = errors.New("未登录或无权限")
|
||||||
|
errOaDocBadID = errors.New("无效ID")
|
||||||
|
errOaDocBadParam = errors.New("参数错误")
|
||||||
|
)
|
||||||
|
|
||||||
|
// BackendOaDocumentController OA 文档管理(文档库 + 文档图谱)。
|
||||||
|
// 数据按租户 tid 隔离;所有业务逻辑委托 services 层,本层只做鉴权、参数解析与响应格式化。
|
||||||
|
type BackendOaDocumentController struct {
|
||||||
|
beego.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare 所有接口执行前确保文档相关表存在。
|
||||||
|
func (c *BackendOaDocumentController) Prepare() {
|
||||||
|
_ = models.EnsureOaDocumentTables()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOaDocumentController) oaDocClaims() (*jwtutil.Claims, error) {
|
||||||
|
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||||
|
if auth == "" {
|
||||||
|
return nil, errOaDocUnauthorized
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(auth, " ", 2)
|
||||||
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
|
return nil, errOaDocUnauthorized
|
||||||
|
}
|
||||||
|
claims, err := jwtutil.ParseToken(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if claims.UserType != "backend" {
|
||||||
|
return nil, errOaDocUnauthorized
|
||||||
|
}
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOaDocumentController) oaDocOk(data interface{}) {
|
||||||
|
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// oaDocFail 将业务错误转换为统一响应;未识别的错误按 500 处理。
|
||||||
|
func (c *BackendOaDocumentController) oaDocFail(err error) {
|
||||||
|
status, code, msg := 400, 400, "操作失败"
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case errOaDocUnauthorized:
|
||||||
|
status, code, msg = 401, 401, "未登录或无权限"
|
||||||
|
case services.ErrDocNotFound, services.ErrDocLinkNotFound,
|
||||||
|
services.ErrDocCategoryNotFound, services.ErrDocCategoryInvalid,
|
||||||
|
services.ErrDocLinkTargetNotFound:
|
||||||
|
status, code, msg = 404, 404, err.Error()
|
||||||
|
case services.ErrDocLinkDuplicate:
|
||||||
|
status, code, msg = 409, 409, err.Error()
|
||||||
|
default:
|
||||||
|
msg = err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Ctx.Output.SetStatus(status)
|
||||||
|
c.Data["json"] = map[string]interface{}{"code": code, "msg": msg}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// oaDocID 解析路径参数 :id
|
||||||
|
func (c *BackendOaDocumentController) oaDocID() (uint64, bool) {
|
||||||
|
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
c.oaDocFail(errOaDocBadID)
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return id, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// oaDocActor 由 JWT 构造文档访问者上下文(含用户、所属部门与是否全权限)。
|
||||||
|
func (c *BackendOaDocumentController) oaDocActor(claims *jwtutil.Claims) services.OaDocActor {
|
||||||
|
return services.NewOaDocActor(claims.TenantId, uint64(claims.UserID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 分类 ----------------
|
||||||
|
|
||||||
|
// Categories GET /backend/oa/document/categories 分类树(含文档数量)
|
||||||
|
func (c *BackendOaDocumentController) Categories() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tree, err := services.OaDocCategoryTree(c.oaDocActor(claims))
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCategory POST /backend/oa/document/categories
|
||||||
|
func (c *BackendOaDocumentController) CreateCategory() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
ParentID uint64 `json:"parent_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
Sort int `json:"sort"`
|
||||||
|
}
|
||||||
|
if !c.oaDocParseBody(&body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := services.OaDocCategoryCreate(claims.TenantId, body.ParentID, body.Name, body.Remark, body.Sort)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCategory POST /backend/oa/document/categories/:id
|
||||||
|
// parent_id < 0 表示不调整父级
|
||||||
|
func (c *BackendOaDocumentController) UpdateCategory() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.oaDocID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
ParentID *int64 `json:"parent_id"`
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Remark *string `json:"remark"`
|
||||||
|
Sort *int `json:"sort"`
|
||||||
|
}
|
||||||
|
if !c.oaDocParseBody(&body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parentID := int64(-1)
|
||||||
|
if body.ParentID != nil {
|
||||||
|
parentID = *body.ParentID
|
||||||
|
}
|
||||||
|
name, remark, sort := "", "", -1
|
||||||
|
if body.Name != nil {
|
||||||
|
name = *body.Name
|
||||||
|
}
|
||||||
|
if body.Remark != nil {
|
||||||
|
remark = *body.Remark
|
||||||
|
}
|
||||||
|
if body.Sort != nil {
|
||||||
|
sort = *body.Sort
|
||||||
|
}
|
||||||
|
item, err := services.OaDocCategoryUpdate(claims.TenantId, id, name, remark, sort, parentID)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCategory DELETE /backend/oa/document/categories/:id
|
||||||
|
func (c *BackendOaDocumentController) DeleteCategory() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.oaDocID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := services.OaDocCategoryDelete(claims.TenantId, id); err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 文档 ----------------
|
||||||
|
|
||||||
|
// List GET /backend/oa/document/list
|
||||||
|
func (c *BackendOaDocumentController) List() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page, _ := c.GetInt("page", 1)
|
||||||
|
pageSize, _ := c.GetInt("pageSize", 20)
|
||||||
|
status, _ := c.GetInt("status", -1)
|
||||||
|
docType, _ := c.GetInt("doc_type", -1)
|
||||||
|
star, _ := c.GetInt("star", -1)
|
||||||
|
categoryID, _ := c.GetUint64("category_id")
|
||||||
|
|
||||||
|
actor := c.oaDocActor(claims)
|
||||||
|
list, total, err := services.OaDocList(actor, services.OaDocListParams{
|
||||||
|
Actor: actor,
|
||||||
|
Keyword: strings.TrimSpace(c.GetString("keyword")),
|
||||||
|
CategoryID: categoryID,
|
||||||
|
Uncategorized: strings.TrimSpace(c.GetString("uncategorized")) == "1",
|
||||||
|
Status: status,
|
||||||
|
DocType: docType,
|
||||||
|
Tag: strings.TrimSpace(c.GetString("tag")),
|
||||||
|
Star: star,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(map[string]interface{}{"list": list, "total": total})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats GET /backend/oa/document/stats 文档库概览
|
||||||
|
func (c *BackendOaDocumentController) Stats() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stats, err := services.OaDocStats(c.oaDocActor(claims))
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail GET /backend/oa/document/detail/:id 详情(累加查看次数)
|
||||||
|
func (c *BackendOaDocumentController) Detail() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.oaDocID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := services.OaDocView(c.oaDocActor(claims), id)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create POST /backend/oa/document/create
|
||||||
|
func (c *BackendOaDocumentController) Create() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, ok := c.oaDocParseSaveBody()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := services.OaDocCreate(c.oaDocActor(claims), claims.Username, body)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update POST /backend/oa/document/update/:id
|
||||||
|
func (c *BackendOaDocumentController) Update() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.oaDocID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, ok := c.oaDocParseSaveBody()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := services.OaDocUpdate(c.oaDocActor(claims), id, body)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete DELETE /backend/oa/document/delete/:id 软删除
|
||||||
|
func (c *BackendOaDocumentController) Delete() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.oaDocID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n, err := services.OaDocDelete(c.oaDocActor(claims), []uint64{id})
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(map[string]interface{}{"count": n})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBatch POST /backend/oa/document/delete-batch 批量软删除
|
||||||
|
func (c *BackendOaDocumentController) DeleteBatch() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
IDs []uint64 `json:"ids"`
|
||||||
|
}
|
||||||
|
if !c.oaDocParseBody(&body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n, err := services.OaDocDelete(c.oaDocActor(claims), body.IDs)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(map[string]interface{}{"count": n})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move POST /backend/oa/document/move 批量移动分类
|
||||||
|
func (c *BackendOaDocumentController) Move() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
IDs []uint64 `json:"ids"`
|
||||||
|
CategoryID uint64 `json:"category_id"`
|
||||||
|
}
|
||||||
|
if !c.oaDocParseBody(&body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n, err := services.OaDocMove(c.oaDocActor(claims), body.IDs, body.CategoryID)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(map[string]interface{}{"count": n})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Star POST /backend/oa/document/star/:id 收藏 / 取消收藏
|
||||||
|
func (c *BackendOaDocumentController) Star() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.oaDocID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isStar, err := services.OaDocToggleStar(c.oaDocActor(claims), id)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(map[string]interface{}{"is_star": isStar})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download POST /backend/oa/document/download/:id 记录下载次数并返回下载地址
|
||||||
|
func (c *BackendOaDocumentController) Download() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.oaDocID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := services.OaDocDownload(c.oaDocActor(claims), id)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(map[string]interface{}{
|
||||||
|
"id": item.ID,
|
||||||
|
"title": item.Title,
|
||||||
|
"file_url": item.FileURL,
|
||||||
|
"file_name": item.FileName,
|
||||||
|
"download_count": item.DownCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 文档关联 ----------------
|
||||||
|
|
||||||
|
// LinkList GET /backend/oa/document/links?doc_id=&all=1
|
||||||
|
func (c *BackendOaDocumentController) LinkList() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
docID, _ := c.GetUint64("doc_id")
|
||||||
|
all := strings.TrimSpace(c.GetString("all")) == "1"
|
||||||
|
|
||||||
|
list, err := services.OaDocLinkList(c.oaDocActor(claims), docID, all)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkCreate POST /backend/oa/document/links
|
||||||
|
func (c *BackendOaDocumentController) LinkCreate() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
SourceID uint64 `json:"source_id"`
|
||||||
|
TargetID uint64 `json:"target_id"`
|
||||||
|
Relation string `json:"relation"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
if !c.oaDocParseBody(&body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := services.OaDocLinkCreate(c.oaDocActor(claims), uint64(claims.UserID),
|
||||||
|
body.SourceID, body.TargetID, body.Relation, body.Remark)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkDelete DELETE /backend/oa/document/links/:id
|
||||||
|
func (c *BackendOaDocumentController) LinkDelete() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.oaDocID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := services.OaDocLinkDelete(c.oaDocActor(claims), id); err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 文档图谱 ----------------
|
||||||
|
|
||||||
|
// Graph GET /backend/oa/document/graph
|
||||||
|
func (c *BackendOaDocumentController) Graph() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
categoryID, _ := c.GetUint64("category_id")
|
||||||
|
limit, _ := c.GetInt("limit", 300)
|
||||||
|
includeCategory := strings.TrimSpace(c.GetString("include_category"))
|
||||||
|
onlyLinked := strings.TrimSpace(c.GetString("only_linked")) == "1"
|
||||||
|
|
||||||
|
result, err := services.OaDocGraph(c.oaDocActor(claims), services.OaDocGraphParams{
|
||||||
|
Keyword: strings.TrimSpace(c.GetString("keyword")),
|
||||||
|
CategoryID: categoryID,
|
||||||
|
Relation: strings.TrimSpace(c.GetString("relation")),
|
||||||
|
IncludeCategory: includeCategory != "0",
|
||||||
|
OnlyLinked: onlyLinked,
|
||||||
|
Limit: limit,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 文档共享(私密文档可见性) ----------------
|
||||||
|
|
||||||
|
// Members GET /backend/oa/document/members 租户用户列表(私密文档共享选人)
|
||||||
|
func (c *BackendOaDocumentController) Members() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||||
|
limit, _ := c.GetInt("limit", 100)
|
||||||
|
members, err := services.OaDocMembers(claims.TenantId, keyword, limit)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(members)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShareList GET /backend/oa/document/shares/:id 读取文档共享清单
|
||||||
|
func (c *BackendOaDocumentController) ShareList() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.oaDocID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, err := services.OaDocShareList(c.oaDocActor(claims), id)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShareSet POST /backend/oa/document/shares/:id 全量设置文档共享(创建者/全权限用户)
|
||||||
|
func (c *BackendOaDocumentController) ShareSet() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.oaDocID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
Shares []services.OaDocShareItem `json:"shares"`
|
||||||
|
}
|
||||||
|
if !c.oaDocParseBody(&body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, err := services.OaDocShareSet(c.oaDocActor(claims), id, body.Shares)
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShareRemove DELETE /backend/oa/document/shares/:sid 删除单条共享
|
||||||
|
func (c *BackendOaDocumentController) ShareRemove() {
|
||||||
|
claims, err := c.oaDocClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sid, err := strconv.ParseUint(c.Ctx.Input.Param(":sid"), 10, 64)
|
||||||
|
if err != nil || sid == 0 {
|
||||||
|
c.oaDocFail(errOaDocBadID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := services.OaDocShareRemove(c.oaDocActor(claims), sid); err != nil {
|
||||||
|
c.oaDocFail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaDocOk(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 辅助 ----------------
|
||||||
|
|
||||||
|
// oaDocParseBody 读取并解析 JSON 请求体
|
||||||
|
func (c *BackendOaDocumentController) oaDocParseBody(out interface{}) bool {
|
||||||
|
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
if err != nil || json.Unmarshal(raw, out) != nil {
|
||||||
|
c.oaDocFail(errOaDocBadParam)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// oaDocParseSaveBody 解析文档新增/编辑请求体
|
||||||
|
func (c *BackendOaDocumentController) oaDocParseSaveBody() (services.OaDocSaveParams, bool) {
|
||||||
|
var body struct {
|
||||||
|
CategoryID *uint64 `json:"category_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
FileID uint64 `json:"file_id"`
|
||||||
|
FileURL string `json:"file_url"`
|
||||||
|
FileName string `json:"file_name"`
|
||||||
|
Ext string `json:"ext"`
|
||||||
|
Size uint64 `json:"size"`
|
||||||
|
Tags string `json:"tags"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
Status *int8 `json:"status"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
OwnerID uint64 `json:"owner_id"`
|
||||||
|
OwnerName string `json:"owner_name"`
|
||||||
|
IsStar *int8 `json:"is_star"`
|
||||||
|
Visibility *int8 `json:"visibility"`
|
||||||
|
}
|
||||||
|
if !c.oaDocParseBody(&body) {
|
||||||
|
return services.OaDocSaveParams{}, false
|
||||||
|
}
|
||||||
|
params := services.OaDocSaveParams{
|
||||||
|
Title: body.Title,
|
||||||
|
FileID: body.FileID,
|
||||||
|
FileURL: body.FileURL,
|
||||||
|
FileName: body.FileName,
|
||||||
|
Ext: body.Ext,
|
||||||
|
Size: body.Size,
|
||||||
|
Tags: body.Tags,
|
||||||
|
Summary: body.Summary,
|
||||||
|
Version: body.Version,
|
||||||
|
OwnerID: body.OwnerID,
|
||||||
|
OwnerName: body.OwnerName,
|
||||||
|
}
|
||||||
|
if body.Visibility != nil {
|
||||||
|
params.Visibility = *body.Visibility
|
||||||
|
}
|
||||||
|
if body.CategoryID != nil {
|
||||||
|
params.SetCategory = true
|
||||||
|
params.CategoryID = *body.CategoryID
|
||||||
|
}
|
||||||
|
// -1 表示"不修改该字段"
|
||||||
|
if body.Status != nil {
|
||||||
|
params.Status = *body.Status
|
||||||
|
} else {
|
||||||
|
params.Status = -1
|
||||||
|
}
|
||||||
|
if body.IsStar != nil {
|
||||||
|
params.IsStar = *body.IsStar
|
||||||
|
} else {
|
||||||
|
params.IsStar = -1
|
||||||
|
}
|
||||||
|
return params, true
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ type tenantUserPayload struct {
|
|||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
IsDefault *int8 `json:"is_default"`
|
IsDefault *int8 `json:"is_default"`
|
||||||
Status *int8 `json:"status"`
|
Status *int8 `json:"status"`
|
||||||
|
OrgID *uint64 `json:"org_id"`
|
||||||
Remark *string `json:"remark"`
|
Remark *string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +201,11 @@ func (c *PlatformTenantUserController) CreateTenantUser() {
|
|||||||
_ = c.ServeJSON()
|
_ = c.ServeJSON()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if p.OrgID != nil && *p.OrgID > 0 {
|
||||||
|
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||||
|
Filter("id", id).
|
||||||
|
Update(map[string]interface{}{"org_id": *p.OrgID})
|
||||||
|
}
|
||||||
if isDefault == 1 {
|
if isDefault == 1 {
|
||||||
_ = services.SetDefaultTenant(p.Uid, p.Tid)
|
_ = services.SetDefaultTenant(p.Uid, p.Tid)
|
||||||
// 租户第一个账号(普通租户)即租户管理员:自动绑定「租户管理员」角色,
|
// 租户第一个账号(普通租户)即租户管理员:自动绑定「租户管理员」角色,
|
||||||
@@ -265,6 +271,9 @@ func (c *PlatformTenantUserController) EditTenantUser() {
|
|||||||
if p.Status != nil {
|
if p.Status != nil {
|
||||||
update["status"] = *p.Status
|
update["status"] = *p.Status
|
||||||
}
|
}
|
||||||
|
if p.OrgID != nil {
|
||||||
|
update["org_id"] = *p.OrgID
|
||||||
|
}
|
||||||
if p.Remark != nil {
|
if p.Remark != nil {
|
||||||
update["remark"] = p.Remark
|
update["remark"] = p.Remark
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
-- OA 文档管理(文档库 / 文档图谱)建表脚本
|
||||||
|
--
|
||||||
|
-- 说明:正常情况【不需要执行本脚本】。
|
||||||
|
-- 服务在首次访问 /backend/oa/document/* 接口时,会由
|
||||||
|
-- controllers/backend_oa_document.go 的 Prepare() 调用
|
||||||
|
-- models.EnsureOaDocumentTables() 自动建表(CREATE TABLE IF NOT EXISTS,
|
||||||
|
-- sync.Once 保证单进程只建一次)。
|
||||||
|
--
|
||||||
|
-- 本脚本仅用于以下场景手工预建:
|
||||||
|
-- 1. 生产库账号没有 DDL 权限,自动建表失败;
|
||||||
|
-- 2. DBA 要求先在预发/生产环境审核建表语句;
|
||||||
|
-- 3. 需要排查表结构是否符合预期。
|
||||||
|
--
|
||||||
|
-- 幂等:全部使用 IF NOT EXISTS,可反复执行,不会覆盖已有数据。
|
||||||
|
-- 与代码保持一致:models/oa_document.go
|
||||||
|
|
||||||
|
-- 1. 文档分类表(树形)
|
||||||
|
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_category (
|
||||||
|
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||||
|
parent_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '父级ID,0为一级分类',
|
||||||
|
name varchar(128) NOT NULL DEFAULT '' COMMENT '分类名称',
|
||||||
|
sort int NOT NULL DEFAULT 0 COMMENT '排序,越小越靠前',
|
||||||
|
remark varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
|
||||||
|
is_deleted tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
|
||||||
|
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
update_time datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
delete_time datetime DEFAULT NULL COMMENT '删除时间',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_tid_parent (tid, parent_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档分类表';
|
||||||
|
|
||||||
|
-- 2. 文档表
|
||||||
|
-- doc_type: 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频
|
||||||
|
-- status : 0-草稿 1-已发布 2-已归档
|
||||||
|
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc (
|
||||||
|
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||||
|
category_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '所属分类ID,0为未分类',
|
||||||
|
title varchar(255) NOT NULL DEFAULT '' COMMENT '文档标题',
|
||||||
|
file_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '关联附件ID(yz_system_files)',
|
||||||
|
file_url varchar(512) NOT NULL DEFAULT '' COMMENT '附件访问地址',
|
||||||
|
file_name varchar(255) NOT NULL DEFAULT '' COMMENT '原始文件名',
|
||||||
|
ext varchar(16) NOT NULL DEFAULT '' COMMENT '扩展名',
|
||||||
|
size bigint unsigned NOT NULL DEFAULT 0 COMMENT '文件大小(字节)',
|
||||||
|
doc_type tinyint NOT NULL DEFAULT 0 COMMENT '类型 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频',
|
||||||
|
tags varchar(255) NOT NULL DEFAULT '' COMMENT '标签,逗号分隔',
|
||||||
|
summary varchar(1000) NOT NULL DEFAULT '' COMMENT '摘要/描述',
|
||||||
|
status tinyint NOT NULL DEFAULT 0 COMMENT '状态 0-草稿 1-已发布 2-已归档',
|
||||||
|
version int NOT NULL DEFAULT 1 COMMENT '版本号',
|
||||||
|
is_star tinyint NOT NULL DEFAULT 0 COMMENT '是否收藏 0-否 1-是',
|
||||||
|
owner_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '负责人ID',
|
||||||
|
owner_name varchar(100) NOT NULL DEFAULT '' COMMENT '负责人姓名',
|
||||||
|
view_count int NOT NULL DEFAULT 0 COMMENT '查看次数',
|
||||||
|
download_count int NOT NULL DEFAULT 0 COMMENT '下载次数',
|
||||||
|
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建人ID',
|
||||||
|
creator_name varchar(100) NOT NULL DEFAULT '' COMMENT '创建人姓名',
|
||||||
|
visibility tinyint NOT NULL DEFAULT 0 COMMENT '可见性 0-租户公开 1-私密(仅创建者与被共享者)',
|
||||||
|
is_deleted tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
|
||||||
|
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
update_time datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
delete_time datetime DEFAULT NULL COMMENT '删除时间',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_tid_cate (tid, category_id, is_deleted),
|
||||||
|
KEY idx_tid_status (tid, status, is_deleted),
|
||||||
|
KEY idx_tid_title (tid, title)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档表';
|
||||||
|
|
||||||
|
-- 3. 文档关联表(文档图谱的连线来源)
|
||||||
|
-- relation: reference-引用 related-相关 version-版本 belong-从属
|
||||||
|
-- 说明:代码在建立关联时会同时写入正向与反向两条记录,便于图谱无向检索;
|
||||||
|
-- uk_doc_pair 保证同一对文档的同一关系不重复。
|
||||||
|
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_link (
|
||||||
|
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||||
|
source_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '源文档ID',
|
||||||
|
target_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '目标文档ID',
|
||||||
|
relation varchar(32) NOT NULL DEFAULT 'related' COMMENT '关系 reference-引用 related-相关 version-版本 belong-从属',
|
||||||
|
remark varchar(255) NOT NULL DEFAULT '' COMMENT '关系说明',
|
||||||
|
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建人ID',
|
||||||
|
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_doc_pair (tid, source_id, target_id, relation),
|
||||||
|
KEY idx_tid_source (tid, source_id),
|
||||||
|
KEY idx_tid_target (tid, target_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档关联表';
|
||||||
|
|
||||||
|
-- 4. 文档共享表(私密文档可见性授权)
|
||||||
|
-- share_type: 0-用户(target_id=uid) 1-部门(target_id=org_id,含子部门)
|
||||||
|
-- uk_doc_target 保证同一文档对同一目标不重复授权。
|
||||||
|
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_share (
|
||||||
|
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||||
|
doc_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '文档ID',
|
||||||
|
share_type tinyint NOT NULL DEFAULT 0 COMMENT '共享类型 0-用户(target_id=uid) 1-部门(target_id=org_id,含子部门)',
|
||||||
|
target_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '共享目标ID',
|
||||||
|
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '授权人ID',
|
||||||
|
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_doc_target (tid, doc_id, share_type, target_id),
|
||||||
|
KEY idx_tid_doc (tid, doc_id),
|
||||||
|
KEY idx_target (tid, share_type, target_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档共享表';
|
||||||
|
|
||||||
|
-- 5. 校验(应看到 4 张表)
|
||||||
|
-- SHOW TABLES LIKE 'yz_backend_oa_doc%';
|
||||||
|
|
||||||
@@ -114,6 +114,10 @@ func Init(_ string) {
|
|||||||
|
|
||||||
new(OaSchedule),
|
new(OaSchedule),
|
||||||
new(OaNotice),
|
new(OaNotice),
|
||||||
|
new(OaDocCategory),
|
||||||
|
new(OaDoc),
|
||||||
|
new(OaDocLink),
|
||||||
|
new(OaDocShare),
|
||||||
new(BackendOaCompensationScheme),
|
new(BackendOaCompensationScheme),
|
||||||
new(BackendOaPayroll),
|
new(BackendOaPayroll),
|
||||||
new(BackendOaPayrollItem),
|
new(BackendOaPayrollItem),
|
||||||
@@ -127,4 +131,5 @@ func Init(_ string) {
|
|||||||
// 补齐历史库缺失的列(已存在时 MySQL 报 duplicate column,统一忽略)
|
// 补齐历史库缺失的列(已存在时 MySQL 报 duplicate column,统一忽略)
|
||||||
EnsureAdminRoleTenantColumn()
|
EnsureAdminRoleTenantColumn()
|
||||||
EnsureAdminRoleCustomColumn()
|
EnsureAdminRoleCustomColumn()
|
||||||
|
EnsureTenantUserOrgColumn()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OA 文档管理(文档库 / 文档图谱)数据模型。
|
||||||
|
//
|
||||||
|
// 三张表按租户 tid 隔离:
|
||||||
|
// - yz_backend_oa_doc_category 文档分类(树形,parent_id=0 为一级)
|
||||||
|
// - yz_backend_oa_doc 文档主体(附件信息冗余存储,避免每次联查文件表)
|
||||||
|
// - yz_backend_oa_doc_link 文档关联关系(引用/相关/版本/从属),文档图谱的连线来源
|
||||||
|
|
||||||
|
// OaDocCategory OA文档分类: yz_backend_oa_doc_category
|
||||||
|
type OaDocCategory struct {
|
||||||
|
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||||
|
Tid int `orm:"column(tid)" json:"tid"`
|
||||||
|
ParentID uint64 `orm:"column(parent_id);default(0)" json:"parent_id"`
|
||||||
|
Name string `orm:"column(name);size(128)" json:"name"`
|
||||||
|
Sort int `orm:"column(sort);default(0)" json:"sort"`
|
||||||
|
Remark string `orm:"column(remark);size(255);default()" json:"remark"`
|
||||||
|
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||||
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||||
|
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
|
||||||
|
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OaDocCategory) TableName() string {
|
||||||
|
return "yz_backend_oa_doc_category"
|
||||||
|
}
|
||||||
|
|
||||||
|
// OaDoc OA文档: yz_backend_oa_doc
|
||||||
|
// status: 0-草稿 1-已发布 2-已归档
|
||||||
|
// doc_type: 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频
|
||||||
|
type OaDoc struct {
|
||||||
|
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||||
|
Tid int `orm:"column(tid)" json:"tid"`
|
||||||
|
CategoryID uint64 `orm:"column(category_id);default(0)" json:"category_id"`
|
||||||
|
Title string `orm:"column(title);size(255)" json:"title"`
|
||||||
|
FileID uint64 `orm:"column(file_id);default(0)" json:"file_id"`
|
||||||
|
FileURL string `orm:"column(file_url);size(512);default()" json:"file_url"`
|
||||||
|
FileName string `orm:"column(file_name);size(255);default()" json:"file_name"`
|
||||||
|
Ext string `orm:"column(ext);size(16);default()" json:"ext"`
|
||||||
|
Size uint64 `orm:"column(size);default(0)" json:"size"`
|
||||||
|
DocType int8 `orm:"column(doc_type);default(0)" json:"doc_type"`
|
||||||
|
Tags string `orm:"column(tags);size(255);default()" json:"tags"`
|
||||||
|
Summary string `orm:"column(summary);size(1000);default()" json:"summary"`
|
||||||
|
Status int8 `orm:"column(status);default(0)" json:"status"`
|
||||||
|
Version int `orm:"column(version);default(1)" json:"version"`
|
||||||
|
IsStar int8 `orm:"column(is_star);default(0)" json:"is_star"`
|
||||||
|
OwnerID uint64 `orm:"column(owner_id);default(0)" json:"owner_id"`
|
||||||
|
OwnerName string `orm:"column(owner_name);size(100);default()" json:"owner_name"`
|
||||||
|
ViewCount int `orm:"column(view_count);default(0)" json:"view_count"`
|
||||||
|
DownCount int `orm:"column(download_count);default(0)" json:"download_count"`
|
||||||
|
CreatorID uint64 `orm:"column(creator_id);default(0)" json:"creator_id"`
|
||||||
|
CreatorName string `orm:"column(creator_name);size(100);default()" json:"creator_name"`
|
||||||
|
// Visibility 可见性:0-租户公开(默认,租户内所有后台用户可见)1-私密(仅创建者与被共享者可见)
|
||||||
|
Visibility int8 `orm:"column(visibility);default(0)" json:"visibility"`
|
||||||
|
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||||
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||||
|
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
|
||||||
|
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OaDoc) TableName() string {
|
||||||
|
return "yz_backend_oa_doc"
|
||||||
|
}
|
||||||
|
|
||||||
|
// OaDocLink OA文档关联: yz_backend_oa_doc_link
|
||||||
|
// relation: reference-引用 related-相关 version-版本迭代 belong-从属
|
||||||
|
type OaDocLink struct {
|
||||||
|
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||||
|
Tid int `orm:"column(tid)" json:"tid"`
|
||||||
|
SourceID uint64 `orm:"column(source_id);default(0)" json:"source_id"`
|
||||||
|
TargetID uint64 `orm:"column(target_id);default(0)" json:"target_id"`
|
||||||
|
Relation string `orm:"column(relation);size(32);default(related)" json:"relation"`
|
||||||
|
Remark string `orm:"column(remark);size(255);default()" json:"remark"`
|
||||||
|
CreatorID uint64 `orm:"column(creator_id);default(0)" json:"creator_id"`
|
||||||
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OaDocLink) TableName() string {
|
||||||
|
return "yz_backend_oa_doc_link"
|
||||||
|
}
|
||||||
|
|
||||||
|
// OaDocShare OA文档共享: yz_backend_oa_doc_share
|
||||||
|
// 私密文档(visibility=1)在"创建者可见"之外,额外授权给指定用户或部门。
|
||||||
|
// share_type: 0-用户(target_id 为 yz_system_tenant_user.uid)
|
||||||
|
// 1-部门(target_id 为 yz_backend_organization.id,含其全部子部门)
|
||||||
|
type OaDocShare struct {
|
||||||
|
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||||
|
Tid int `orm:"column(tid)" json:"tid"`
|
||||||
|
DocID uint64 `orm:"column(doc_id);default(0)" json:"doc_id"`
|
||||||
|
ShareType int8 `orm:"column(share_type);default(0)" json:"share_type"`
|
||||||
|
TargetID uint64 `orm:"column(target_id);default(0)" json:"target_id"`
|
||||||
|
CreatorID uint64 `orm:"column(creator_id);default(0)" json:"creator_id"`
|
||||||
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OaDocShare) TableName() string {
|
||||||
|
return "yz_backend_oa_doc_share"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文档类型常量(doc_type)
|
||||||
|
const (
|
||||||
|
DocTypeOther int8 = 0
|
||||||
|
DocTypeDoc int8 = 1 // doc / docx / txt / md / rtf
|
||||||
|
DocTypeSheet int8 = 2 // xls / xlsx / csv
|
||||||
|
DocTypeSlide int8 = 3 // ppt / pptx
|
||||||
|
DocTypePDF int8 = 4 // pdf
|
||||||
|
DocTypeImage int8 = 5 // jpg / png / gif / bmp / webp
|
||||||
|
DocTypeZip int8 = 6 // zip / rar / 7z / tar / gz
|
||||||
|
DocTypeVideo int8 = 7
|
||||||
|
DocTypeAudio int8 = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
// 文档状态常量(status)
|
||||||
|
const (
|
||||||
|
DocStatusDraft int8 = 0
|
||||||
|
DocStatusPublish int8 = 1
|
||||||
|
DocStatusArchive int8 = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// 文档关联关系常量(relation)
|
||||||
|
const (
|
||||||
|
DocRelationReference = "reference"
|
||||||
|
DocRelationRelated = "related"
|
||||||
|
DocRelationVersion = "version"
|
||||||
|
DocRelationBelong = "belong"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 文档可见性常量(visibility)
|
||||||
|
const (
|
||||||
|
DocVisibilityPublic int8 = 0 // 租户公开
|
||||||
|
DocVisibilityPrivate int8 = 1 // 私密
|
||||||
|
)
|
||||||
|
|
||||||
|
// 文档共享目标类型常量(share_type)
|
||||||
|
const (
|
||||||
|
DocShareTypeUser int8 = 0 // 共享给指定用户(target_id = yz_system_tenant_user.uid)
|
||||||
|
DocShareTypeOrg int8 = 1 // 共享给部门(target_id = yz_backend_organization.id,含子部门)
|
||||||
|
)
|
||||||
|
|
||||||
|
// extDocTypeMap 扩展名 -> 文档类型
|
||||||
|
var extDocTypeMap = map[string]int8{
|
||||||
|
"doc": DocTypeDoc, "docx": DocTypeDoc, "txt": DocTypeDoc, "md": DocTypeDoc,
|
||||||
|
"rtf": DocTypeDoc, "wps": DocTypeDoc, "odt": DocTypeDoc,
|
||||||
|
"xls": DocTypeSheet, "xlsx": DocTypeSheet, "csv": DocTypeSheet, "ods": DocTypeSheet,
|
||||||
|
"ppt": DocTypeSlide, "pptx": DocTypeSlide, "odp": DocTypeSlide,
|
||||||
|
"pdf": DocTypePDF,
|
||||||
|
"jpg": DocTypeImage, "jpeg": DocTypeImage, "png": DocTypeImage,
|
||||||
|
"gif": DocTypeImage, "bmp": DocTypeImage, "webp": DocTypeImage, "svg": DocTypeImage,
|
||||||
|
"zip": DocTypeZip, "rar": DocTypeZip, "7z": DocTypeZip, "tar": DocTypeZip, "gz": DocTypeZip,
|
||||||
|
"mp4": DocTypeVideo, "webm": DocTypeVideo, "mov": DocTypeVideo, "avi": DocTypeVideo,
|
||||||
|
"mp3": DocTypeAudio, "wav": DocTypeAudio, "ogg": DocTypeAudio,
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocTypeByExt 根据扩展名推断文档类型,未知返回"其他"。
|
||||||
|
func DocTypeByExt(ext string) int8 {
|
||||||
|
if t, ok := extDocTypeMap[strings.ToLower(strings.TrimPrefix(strings.TrimSpace(ext), "."))]; ok {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return DocTypeOther
|
||||||
|
}
|
||||||
|
|
||||||
|
var oaDocumentTableOnce sync.Once
|
||||||
|
|
||||||
|
// EnsureOaDocumentTables 首次访问文档接口时自动建表(若不存在)。
|
||||||
|
// 与通知公告/日程保持一致的运行期自愈策略,避免新功能上线还需手工执行建表脚本。
|
||||||
|
func EnsureOaDocumentTables() error {
|
||||||
|
if Orm == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
oaDocumentTableOnce.Do(func() {
|
||||||
|
_, err = Orm.Raw(`
|
||||||
|
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_category (
|
||||||
|
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||||
|
parent_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '父级ID,0为一级分类',
|
||||||
|
name varchar(128) NOT NULL DEFAULT '' COMMENT '分类名称',
|
||||||
|
sort int NOT NULL DEFAULT 0 COMMENT '排序,越小越靠前',
|
||||||
|
remark varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
|
||||||
|
is_deleted tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
|
||||||
|
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
update_time datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
delete_time datetime DEFAULT NULL COMMENT '删除时间',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_tid_parent (tid, parent_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档分类表'`).Exec()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = Orm.Raw(`
|
||||||
|
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc (
|
||||||
|
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||||
|
category_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '所属分类ID,0为未分类',
|
||||||
|
title varchar(255) NOT NULL DEFAULT '' COMMENT '文档标题',
|
||||||
|
file_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '关联附件ID(yz_system_files)',
|
||||||
|
file_url varchar(512) NOT NULL DEFAULT '' COMMENT '附件访问地址',
|
||||||
|
file_name varchar(255) NOT NULL DEFAULT '' COMMENT '原始文件名',
|
||||||
|
ext varchar(16) NOT NULL DEFAULT '' COMMENT '扩展名',
|
||||||
|
size bigint unsigned NOT NULL DEFAULT 0 COMMENT '文件大小(字节)',
|
||||||
|
doc_type tinyint NOT NULL DEFAULT 0 COMMENT '类型 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频',
|
||||||
|
tags varchar(255) NOT NULL DEFAULT '' COMMENT '标签,逗号分隔',
|
||||||
|
summary varchar(1000) NOT NULL DEFAULT '' COMMENT '摘要/描述',
|
||||||
|
status tinyint NOT NULL DEFAULT 0 COMMENT '状态 0-草稿 1-已发布 2-已归档',
|
||||||
|
version int NOT NULL DEFAULT 1 COMMENT '版本号',
|
||||||
|
is_star tinyint NOT NULL DEFAULT 0 COMMENT '是否收藏 0-否 1-是',
|
||||||
|
owner_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '负责人ID',
|
||||||
|
owner_name varchar(100) NOT NULL DEFAULT '' COMMENT '负责人姓名',
|
||||||
|
view_count int NOT NULL DEFAULT 0 COMMENT '查看次数',
|
||||||
|
download_count int NOT NULL DEFAULT 0 COMMENT '下载次数',
|
||||||
|
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建人ID',
|
||||||
|
creator_name varchar(100) NOT NULL DEFAULT '' COMMENT '创建人姓名',
|
||||||
|
visibility tinyint NOT NULL DEFAULT 0 COMMENT '可见性 0-租户公开 1-私密(仅创建者与被共享者)',
|
||||||
|
is_deleted tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
|
||||||
|
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
update_time datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
delete_time datetime DEFAULT NULL COMMENT '删除时间',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_tid_cate (tid, category_id, is_deleted),
|
||||||
|
KEY idx_tid_status (tid, status, is_deleted),
|
||||||
|
KEY idx_tid_title (tid, title)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档表'`).Exec()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = Orm.Raw(`
|
||||||
|
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_link (
|
||||||
|
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||||
|
source_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '源文档ID',
|
||||||
|
target_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '目标文档ID',
|
||||||
|
relation varchar(32) NOT NULL DEFAULT 'related' COMMENT '关系 reference-引用 related-相关 version-版本 belong-从属',
|
||||||
|
remark varchar(255) NOT NULL DEFAULT '' COMMENT '关系说明',
|
||||||
|
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建人ID',
|
||||||
|
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_doc_pair (tid, source_id, target_id, relation),
|
||||||
|
KEY idx_tid_source (tid, source_id),
|
||||||
|
KEY idx_tid_target (tid, target_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档关联表'`).Exec()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 历史库补齐 visibility 列(已存在时 MySQL 报 duplicate column,忽略)
|
||||||
|
_, _ = Orm.Raw(`
|
||||||
|
ALTER TABLE yz_backend_oa_doc
|
||||||
|
ADD COLUMN visibility tinyint NOT NULL DEFAULT 0 COMMENT '可见性 0-租户公开 1-私密(仅创建者与被共享者)'
|
||||||
|
`).Exec()
|
||||||
|
|
||||||
|
_, err = Orm.Raw(`
|
||||||
|
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_share (
|
||||||
|
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||||
|
doc_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '文档ID',
|
||||||
|
share_type tinyint NOT NULL DEFAULT 0 COMMENT '共享类型 0-用户(target_id=uid) 1-部门(target_id=org_id,含子部门)',
|
||||||
|
target_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '共享目标ID',
|
||||||
|
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '授权人ID',
|
||||||
|
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_doc_target (tid, doc_id, share_type, target_id),
|
||||||
|
KEY idx_tid_doc (tid, doc_id),
|
||||||
|
KEY idx_target (tid, share_type, target_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档共享表'`).Exec()
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -19,6 +19,9 @@ type SystemTenantUser struct {
|
|||||||
Password *string `orm:"column(password);size(255);null" json:"password"`
|
Password *string `orm:"column(password);size(255);null" json:"password"`
|
||||||
IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"`
|
IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"`
|
||||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||||
|
// OrgID 所属组织(部门),关联 yz_backend_organization(id);0 表示未分配。
|
||||||
|
// 用于文档私密共享等需要按部门判定可见性的场景。
|
||||||
|
OrgID uint64 `orm:"column(org_id);default(0)" json:"org_id"`
|
||||||
Remark *string `orm:"column(remark);size(255);null" json:"remark"`
|
Remark *string `orm:"column(remark);size(255);null" json:"remark"`
|
||||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||||
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
|
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
|
||||||
@@ -44,3 +47,19 @@ func EnsureTenantUserGroupColumn() {
|
|||||||
`).Exec()
|
`).Exec()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tenantUserOrgColOnce sync.Once
|
||||||
|
|
||||||
|
// EnsureTenantUserOrgColumn 为租户用户表补齐 org_id(所属部门)列。
|
||||||
|
// 与 group_id 同样的幂等策略:字段已存在时 MySQL 报 duplicate column,统一忽略。
|
||||||
|
func EnsureTenantUserOrgColumn() {
|
||||||
|
if Orm == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tenantUserOrgColOnce.Do(func() {
|
||||||
|
_, _ = Orm.Raw(`
|
||||||
|
ALTER TABLE yz_system_tenant_user
|
||||||
|
ADD COLUMN org_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '所属组织ID 关联 yz_backend_organization(id),0-未分配'
|
||||||
|
`).Exec()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -296,6 +296,26 @@ func RegisterAuthRoutes() {
|
|||||||
beego.Router("/backend/oa/notice/delete/:id", &controllers.BackendOaNoticeController{}, "delete:Delete")
|
beego.Router("/backend/oa/notice/delete/:id", &controllers.BackendOaNoticeController{}, "delete:Delete")
|
||||||
beego.Router("/backend/oa/notice/publish/:id", &controllers.BackendOaNoticeController{}, "post:Publish")
|
beego.Router("/backend/oa/notice/publish/:id", &controllers.BackendOaNoticeController{}, "post:Publish")
|
||||||
beego.Router("/backend/oa/notice/top/:id", &controllers.BackendOaNoticeController{}, "post:Top")
|
beego.Router("/backend/oa/notice/top/:id", &controllers.BackendOaNoticeController{}, "post:Top")
|
||||||
|
|
||||||
|
// OA文档管理(文档库 / 文档图谱)
|
||||||
|
beego.Router("/backend/oa/document/categories", &controllers.BackendOaDocumentController{}, "get:Categories;post:CreateCategory")
|
||||||
|
beego.Router("/backend/oa/document/categories/:id", &controllers.BackendOaDocumentController{}, "post:UpdateCategory;delete:DeleteCategory")
|
||||||
|
beego.Router("/backend/oa/document/list", &controllers.BackendOaDocumentController{}, "get:List")
|
||||||
|
beego.Router("/backend/oa/document/stats", &controllers.BackendOaDocumentController{}, "get:Stats")
|
||||||
|
beego.Router("/backend/oa/document/graph", &controllers.BackendOaDocumentController{}, "get:Graph")
|
||||||
|
beego.Router("/backend/oa/document/links", &controllers.BackendOaDocumentController{}, "get:LinkList;post:LinkCreate")
|
||||||
|
beego.Router("/backend/oa/document/links/:id", &controllers.BackendOaDocumentController{}, "delete:LinkDelete")
|
||||||
|
beego.Router("/backend/oa/document/detail/:id", &controllers.BackendOaDocumentController{}, "get:Detail")
|
||||||
|
beego.Router("/backend/oa/document/create", &controllers.BackendOaDocumentController{}, "post:Create")
|
||||||
|
beego.Router("/backend/oa/document/update/:id", &controllers.BackendOaDocumentController{}, "post:Update")
|
||||||
|
beego.Router("/backend/oa/document/delete/:id", &controllers.BackendOaDocumentController{}, "delete:Delete")
|
||||||
|
beego.Router("/backend/oa/document/delete-batch", &controllers.BackendOaDocumentController{}, "post:DeleteBatch")
|
||||||
|
beego.Router("/backend/oa/document/move", &controllers.BackendOaDocumentController{}, "post:Move")
|
||||||
|
beego.Router("/backend/oa/document/star/:id", &controllers.BackendOaDocumentController{}, "post:Star")
|
||||||
|
beego.Router("/backend/oa/document/download/:id", &controllers.BackendOaDocumentController{}, "post:Download")
|
||||||
|
beego.Router("/backend/oa/document/members", &controllers.BackendOaDocumentController{}, "get:Members")
|
||||||
|
beego.Router("/backend/oa/document/shares/:id", &controllers.BackendOaDocumentController{}, "get:ShareList;post:ShareSet")
|
||||||
|
beego.Router("/backend/oa/document/share/:sid", &controllers.BackendOaDocumentController{}, "delete:ShareRemove")
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerOrganizationRoutes 为指定模块前缀注册组织架构路由。
|
// registerOrganizationRoutes 为指定模块前缀注册组织架构路由。
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,11 @@
|
|||||||
<el-table-column prop="name" label="姓名" min-width="120" align="center" />
|
<el-table-column prop="name" label="姓名" min-width="120" align="center" />
|
||||||
<el-table-column prop="phone" label="手机号" min-width="140" align="center" />
|
<el-table-column prop="phone" label="手机号" min-width="140" align="center" />
|
||||||
<el-table-column prop="email" label="邮箱" min-width="180" align="center" />
|
<el-table-column prop="email" label="邮箱" min-width="180" align="center" />
|
||||||
|
<el-table-column prop="org_id" label="部门" min-width="160" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ orgNameMap[Number(row.org_id)] || (Number(row.org_id) ? `部门${row.org_id}` : "未分配") }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="status" label="状态" width="90" align="center">
|
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="Number(row.status) === 1 ? 'success' : 'danger'">
|
<el-tag :type="Number(row.status) === 1 ? 'success' : 'danger'">
|
||||||
@@ -35,8 +40,9 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="260" align="center" fixed="right">
|
<el-table-column label="操作" width="320" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<el-button text type="primary" @click="openEditDialog(row)">编辑</el-button>
|
||||||
<el-button text type="primary" @click="openPasswordDialog(row)">
|
<el-button text type="primary" @click="openPasswordDialog(row)">
|
||||||
修改密码
|
修改密码
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -54,6 +60,45 @@
|
|||||||
|
|
||||||
<AddUser ref="addUserRef" @success="refreshTenantUsers" />
|
<AddUser ref="addUserRef" @success="refreshTenantUsers" />
|
||||||
|
|
||||||
|
<el-dialog v-model="editDialogVisible" title="编辑用户" width="520px" destroy-on-close>
|
||||||
|
<el-form :model="editForm" label-width="90px">
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input v-model="editForm.account" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="姓名">
|
||||||
|
<el-input v-model="editForm.name" placeholder="请输入姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号">
|
||||||
|
<el-input v-model="editForm.phone" placeholder="请输入手机号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="邮箱">
|
||||||
|
<el-input v-model="editForm.email" placeholder="请输入邮箱" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="editForm.status">
|
||||||
|
<el-radio :label="1">启用</el-radio>
|
||||||
|
<el-radio :label="0">禁用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="所属部门">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="editForm.org_id"
|
||||||
|
:data="orgTree"
|
||||||
|
:props="{ label: 'label', value: 'value', children: 'children' }"
|
||||||
|
value-key="value"
|
||||||
|
placeholder="请选择所属部门(用于文档私密共享按部门可见)"
|
||||||
|
clearable
|
||||||
|
check-strictly
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="editSubmitting" @click="submitEdit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="passwordDialogVisible" title="修改密码" width="420px" destroy-on-close>
|
<el-dialog v-model="passwordDialogVisible" title="修改密码" width="420px" destroy-on-close>
|
||||||
<el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules" label-width="90px">
|
<el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules" label-width="90px">
|
||||||
<el-form-item label="新密码" prop="password">
|
<el-form-item label="新密码" prop="password">
|
||||||
@@ -76,6 +121,7 @@ import { ref, reactive, watch } from "vue";
|
|||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { Plus } from "@element-plus/icons-vue";
|
import { Plus } from "@element-plus/icons-vue";
|
||||||
import { getTenantUserList, editTenantUser, deleteTenantUser } from "@/api/tenantUser";
|
import { getTenantUserList, editTenantUser, deleteTenantUser } from "@/api/tenantUser";
|
||||||
|
import { getOrganizationList } from "@/api/erp";
|
||||||
import AddUser from "./adduser.vue";
|
import AddUser from "./adduser.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -88,6 +134,21 @@ const usersLoading = ref(false);
|
|||||||
const userSearchKeyword = ref("");
|
const userSearchKeyword = ref("");
|
||||||
const addUserRef = ref<{ open: (tenantId: number) => void } | null>(null);
|
const addUserRef = ref<{ open: (tenantId: number) => void } | null>(null);
|
||||||
|
|
||||||
|
const orgTree = ref<any[]>([]);
|
||||||
|
const orgNameMap = ref<Record<number, string>>({});
|
||||||
|
|
||||||
|
const editDialogVisible = ref(false);
|
||||||
|
const editSubmitting = ref(false);
|
||||||
|
const editForm = reactive({
|
||||||
|
id: 0,
|
||||||
|
account: "",
|
||||||
|
name: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
status: 1,
|
||||||
|
org_id: undefined as number | undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const passwordDialogVisible = ref(false);
|
const passwordDialogVisible = ref(false);
|
||||||
const passwordSubmitting = ref(false);
|
const passwordSubmitting = ref(false);
|
||||||
const currentTenantUserId = ref<number | null>(null);
|
const currentTenantUserId = ref<number | null>(null);
|
||||||
@@ -122,6 +183,41 @@ const buildTenantUserQuery = (tid: number) => {
|
|||||||
return params;
|
return params;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 将平铺的组织机构列表转换为树结构,供部门选择器使用
|
||||||
|
const buildOrgTree = (list: any[]): any[] => {
|
||||||
|
const nodes = (list || []).map((o) => ({
|
||||||
|
value: Number(o.id),
|
||||||
|
label: o.org_name || o.name || `部门${o.id}`,
|
||||||
|
parent_id: Number(o.parent_id || 0),
|
||||||
|
}));
|
||||||
|
const map = new Map<number, any>();
|
||||||
|
nodes.forEach((n) => map.set(n.value, { ...n, children: [] as any[] }));
|
||||||
|
const tree: any[] = [];
|
||||||
|
map.forEach((node) => {
|
||||||
|
const parent = map.get(node.parent_id);
|
||||||
|
if (parent) parent.children.push(node);
|
||||||
|
else tree.push(node);
|
||||||
|
});
|
||||||
|
return tree;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadOrgs = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getOrganizationList();
|
||||||
|
if (res?.code === 200) {
|
||||||
|
const list = res?.data?.list || res?.data || [];
|
||||||
|
orgTree.value = buildOrgTree(list);
|
||||||
|
const nameMap: Record<number, string> = {};
|
||||||
|
(list || []).forEach((o: any) => {
|
||||||
|
nameMap[Number(o.id)] = o.org_name || o.name || `部门${o.id}`;
|
||||||
|
});
|
||||||
|
orgNameMap.value = nameMap;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
orgTree.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const refreshTenantUsers = async () => {
|
const refreshTenantUsers = async () => {
|
||||||
const id = props.tid;
|
const id = props.tid;
|
||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
@@ -160,6 +256,7 @@ watch(
|
|||||||
() => props.tid,
|
() => props.tid,
|
||||||
(id) => {
|
(id) => {
|
||||||
userSearchKeyword.value = "";
|
userSearchKeyword.value = "";
|
||||||
|
loadOrgs();
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
tenantUsers.value = [];
|
tenantUsers.value = [];
|
||||||
return;
|
return;
|
||||||
@@ -191,6 +288,42 @@ const openPasswordDialog = (row: any) => {
|
|||||||
passwordDialogVisible.value = true;
|
passwordDialogVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEditDialog = (row: any) => {
|
||||||
|
editForm.id = Number(row?.id || 0);
|
||||||
|
editForm.account = row?.account || "";
|
||||||
|
editForm.name = row?.name || "";
|
||||||
|
editForm.phone = row?.phone || "";
|
||||||
|
editForm.email = row?.email || "";
|
||||||
|
editForm.status = Number(row?.status ?? 1) === 1 ? 1 : 0;
|
||||||
|
editForm.org_id = row?.org_id ? Number(row.org_id) : undefined;
|
||||||
|
editDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitEdit = async () => {
|
||||||
|
if (!editForm.id) return;
|
||||||
|
editSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
const res = await editTenantUser(editForm.id, {
|
||||||
|
name: editForm.name,
|
||||||
|
phone: editForm.phone,
|
||||||
|
email: editForm.email,
|
||||||
|
status: editForm.status,
|
||||||
|
org_id: editForm.org_id || 0,
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("保存成功");
|
||||||
|
editDialogVisible.value = false;
|
||||||
|
refreshTenantUsers();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || "保存失败");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message || "保存失败");
|
||||||
|
} finally {
|
||||||
|
editSubmitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitPassword = async () => {
|
const submitPassword = async () => {
|
||||||
if (!passwordFormRef.value || !currentTenantUserId.value) return;
|
if (!passwordFormRef.value || !currentTenantUserId.value) return;
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,18 @@
|
|||||||
<el-radio :label="0">禁用</el-radio>
|
<el-radio :label="0">禁用</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="所属部门" prop="org_id">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="formData.org_id"
|
||||||
|
:data="orgTree"
|
||||||
|
:props="{ label: 'label', value: 'value', children: 'children' }"
|
||||||
|
value-key="value"
|
||||||
|
placeholder="请选择所属部门(用于文档私密共享按部门可见)"
|
||||||
|
clearable
|
||||||
|
check-strictly
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -41,9 +53,10 @@
|
|||||||
* 租户详情内「添加用户」:提交到 /platform/tenantUser/create,
|
* 租户详情内「添加用户」:提交到 /platform/tenantUser/create,
|
||||||
* 后端写入 yz_tenant_user(租户-用户绑定,含冗余账号/密码等字段)。
|
* 后端写入 yz_tenant_user(租户-用户绑定,含冗余账号/密码等字段)。
|
||||||
*/
|
*/
|
||||||
import { ref, reactive } from 'vue';
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { createTenantUser } from '@/api/tenantUser';
|
import { createTenantUser } from '@/api/tenantUser';
|
||||||
|
import { getOrganizationList } from '@/api/erp';
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
@@ -52,6 +65,8 @@ const submitting = ref(false);
|
|||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
const currentTenantId = ref<number | null>(null);
|
const currentTenantId = ref<number | null>(null);
|
||||||
|
|
||||||
|
const orgTree = ref<any[]>([]);
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
account: '',
|
account: '',
|
||||||
password: '',
|
password: '',
|
||||||
@@ -59,9 +74,42 @@ const formData = reactive({
|
|||||||
name: '',
|
name: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
email: '',
|
email: '',
|
||||||
status: 1
|
status: 1,
|
||||||
|
org_id: undefined as number | undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 将平铺的组织机构列表转换为树结构,供部门选择器使用
|
||||||
|
const buildOrgTree = (list: any[]): any[] => {
|
||||||
|
const nodes = (list || []).map((o) => ({
|
||||||
|
value: Number(o.id),
|
||||||
|
label: o.org_name || o.name || `部门${o.id}`,
|
||||||
|
parent_id: Number(o.parent_id || 0),
|
||||||
|
}));
|
||||||
|
const map = new Map<number, any>();
|
||||||
|
nodes.forEach((n) => map.set(n.value, { ...n, children: [] as any[] }));
|
||||||
|
const tree: any[] = [];
|
||||||
|
map.forEach((node) => {
|
||||||
|
const parent = map.get(node.parent_id);
|
||||||
|
if (parent) parent.children.push(node);
|
||||||
|
else tree.push(node);
|
||||||
|
});
|
||||||
|
return tree;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadOrgs = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getOrganizationList();
|
||||||
|
if (res?.code === 200) {
|
||||||
|
const list = res?.data?.list || res?.data || [];
|
||||||
|
orgTree.value = buildOrgTree(list);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
orgTree.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(loadOrgs);
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
account: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
account: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
password: [{ required: true, message: '请输入密码', trigger: 'blur', min: 5 }],
|
password: [{ required: true, message: '请输入密码', trigger: 'blur', min: 5 }],
|
||||||
@@ -100,7 +148,8 @@ const open = (tenantId: number) => {
|
|||||||
name: '',
|
name: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
email: '',
|
email: '',
|
||||||
status: 1
|
status: 1,
|
||||||
|
org_id: undefined
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,6 +169,7 @@ const submitForm = async () => {
|
|||||||
const submitData = {
|
const submitData = {
|
||||||
...formData,
|
...formData,
|
||||||
tid: currentTenantId.value,
|
tid: currentTenantId.value,
|
||||||
|
org_id: formData.org_id || 0,
|
||||||
};
|
};
|
||||||
// 提交时不需要把确认密码发给后端
|
// 提交时不需要把确认密码发给后端
|
||||||
delete (submitData as any).password2;
|
delete (submitData as any).password2;
|
||||||
|
|||||||
Reference in New Issue
Block a user