批量优化
This commit is contained in:
1 parent
438145cae7
commit
c925b198d1
50 files changed
+5716
-291
No files matched your search
@@ -0,0 +1,27 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 获取「加入企业」申请列表(仅本租户,按 JWT tid 隔离)
|
||||
export function getTenantApplies(params) {
|
||||
return request({
|
||||
url: '/backend/tenantApplies',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 通过申请:申请人加入本企业(默认「租户用户」角色)
|
||||
export function approveTenantApply(id) {
|
||||
return request({
|
||||
url: `/backend/tenantApplies/approve/${id}`,
|
||||
method: 'post',
|
||||
});
|
||||
}
|
||||
|
||||
// 驳回申请:data = { reason }
|
||||
export function rejectTenantApply(id, data) {
|
||||
return request({
|
||||
url: `/backend/tenantApplies/reject/${id}`,
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
+14
-8
@@ -18,10 +18,13 @@ export function getTenantUsers(tenantId) {
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export function getUserInfo(userId) {
|
||||
// params.by === 'id':按 yz_system_tenant_user 主键精确查询(用户管理页传列表行 id 时必须携带);
|
||||
// 缺省时后端兼容 identity_id / 老 uid(个人中心等场景)
|
||||
export function getUserInfo(userId, params) {
|
||||
return request({
|
||||
url: `/backend/getUserInfo/${userId}`,
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,12 +37,13 @@ export function addUser(data) {
|
||||
});
|
||||
}
|
||||
|
||||
// 编辑用户信息
|
||||
export function editUser(userId, data) {
|
||||
// 编辑用户信息(params.by === 'id' 时后端按主键精确匹配)
|
||||
export function editUser(userId, data, params) {
|
||||
return request({
|
||||
url: `/backend/editUser/${userId}`,
|
||||
method: 'post',
|
||||
data,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,22 +79,24 @@ export function uploadAvatar(formData) {
|
||||
});
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
export function deleteUser(userId) {
|
||||
// 删除用户(params.by === 'id' 时后端按主键精确匹配)
|
||||
export function deleteUser(userId, params) {
|
||||
return request({
|
||||
url: `/backend/deleteUser/${userId}`,
|
||||
method: 'delete',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
export function changePassword(userId, data) {
|
||||
// 修改密码;byID=true 表示 userId 是用户管理列表行主键,后端按主键精确匹配
|
||||
export function changePassword(userId, data, byID = false) {
|
||||
return request({
|
||||
url: '/backend/changePassword',
|
||||
method: 'post',
|
||||
data: {
|
||||
id: userId,
|
||||
password: data.newPassword
|
||||
password: data.newPassword,
|
||||
by: byID ? 'id' : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -144,29 +144,43 @@
|
||||
<span class="hint">判断节点:拖出多条连线,点击连线分别填写分支条件(留空为默认分支)。运行期按条件命中其中一条分支。</span>
|
||||
</el-form-item>
|
||||
<template v-if="nodeForm.nodeType === 5">
|
||||
<el-form-item label="通知类型">
|
||||
<el-radio-group v-model="nodeForm.approverType" @change="onApproverTypeChange">
|
||||
<el-radio v-for="t in approverTypes" :key="t.value" :value="t.value">{{ t.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="currentApproverMeta && currentApproverMeta.needsSelection" :label="currentApproverMeta.selectLabel" required>
|
||||
<el-select v-model="nodeForm.approverIds" multiple filterable style="width: 100%" :placeholder="'选择' + currentApproverMeta.selectLabel + '(可多选)'">
|
||||
<el-option v-for="opt in currentPool" :key="opt.id" :label="opt.name" :value="opt.id" />
|
||||
<el-form-item label="通知类型" required>
|
||||
<el-select
|
||||
v-model="nodeForm.notifyTypes"
|
||||
multiple
|
||||
collapse-tags
|
||||
style="width: 100%"
|
||||
placeholder="选择通知类型(可多选)"
|
||||
@change="onNotifyTypesChange"
|
||||
>
|
||||
<el-option v-for="t in approverTypes" :key="t.value" :label="t.label" :value="t.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-else label=" ">
|
||||
<span class="hint">该类型运行期自动定位通知对象,无需指定</span>
|
||||
<el-form-item
|
||||
v-for="t in selectedNotifyMetas"
|
||||
:key="t.value"
|
||||
:label="t.selectLabel || t.label"
|
||||
required
|
||||
>
|
||||
<el-select v-model="nodeForm.notifyIdsMap[t.value]" multiple filterable style="width: 100%" :placeholder="'选择' + (t.selectLabel || t.label) + '(可多选)'">
|
||||
<el-option v-for="opt in poolOf(t.poolKey)" :key="opt.id" :label="opt.name" :value="opt.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!selectedNotifyMetas.length" label=" ">
|
||||
<span class="hint">所选类型运行期自动定位通知对象,无需指定</span>
|
||||
</el-form-item>
|
||||
<el-form-item label=" ">
|
||||
<span class="hint">通知节点不阻塞流程,仅发送消息给指定人员,流程自动继续往下走。</span>
|
||||
<span class="hint">通知节点不阻塞流程,流程自动继续往下走。可同时选择多种类型(如「发起人本人」+「指定用户」)。</span>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="danger" plain :disabled="!editingNodeId" @click="removeCurrentNode">删除节点</el-button>
|
||||
<span style="flex: 1"></span>
|
||||
<el-button @click="nodeDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmNode">确定</el-button>
|
||||
<div class="node-dialog-footer">
|
||||
<el-button type="danger" plain :disabled="!editingNodeId" @click="removeCurrentNode">删除节点</el-button>
|
||||
<span class="footer-spacer"></span>
|
||||
<el-button @click="nodeDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmNode">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -249,7 +263,19 @@ function approverSummary(node: any) {
|
||||
return `${approverTypeLabel(node.approverType)}:${names.length ? names.join('、') : '未选'}(${mode})`
|
||||
}
|
||||
if (node.nodeType === NODE_TYPE.DECISION) return '按分支条件路由'
|
||||
if (node.nodeType === NODE_TYPE.NOTIFY) return '发送通知给指定人员'
|
||||
if (node.nodeType === NODE_TYPE.NOTIFY) {
|
||||
const types: number[] = node.notifyTypes?.length ? node.notifyTypes : [node.approverType]
|
||||
const parts = types.map((t) => {
|
||||
const meta = (props.approverTypes as any[]).find((x) => x.value === t)
|
||||
if (!meta) return '未知类型'
|
||||
if (!meta.needsSelection) return meta.label
|
||||
const pool = (props.pools as any)[meta.poolKey] || []
|
||||
const ids = (node.notifyIdsMap || {})[t] || []
|
||||
const names = ids.map((id: number) => pool.find((p: any) => p.id === id)?.name).filter(Boolean)
|
||||
return `${meta.label}:${names.length ? names.join('、') : '未选'}`
|
||||
})
|
||||
return '通知 ' + parts.join(' + ')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -279,7 +305,7 @@ function buildGraph(payload: any) {
|
||||
|
||||
function makeNodeData(n: any) {
|
||||
const nodeType = n.node_type || n.nodeType || NODE_TYPE.APPROVAL
|
||||
const data = {
|
||||
const data: any = {
|
||||
nodeType,
|
||||
cls: clsOf(nodeType),
|
||||
name: n.node_name || n.name || '',
|
||||
@@ -289,10 +315,44 @@ function makeNodeData(n: any) {
|
||||
allowReturn: n.allow_return ?? n.allowReturn ?? 0,
|
||||
timeoutHours: n.timeout_hours ?? n.timeoutHours ?? 48,
|
||||
}
|
||||
if (nodeType === NODE_TYPE.NOTIFY) {
|
||||
applyNotifyData(data, n)
|
||||
}
|
||||
data.summary = approverSummary(data)
|
||||
return data
|
||||
}
|
||||
|
||||
// 通知节点:解析多类型存储 notify_targets: ["1:52","5:0"];兼容旧单类型格式
|
||||
//(纯数字 approver_ids + approver_type),并统一归一化为 notifyTargets 字符串数组。
|
||||
function applyNotifyData(data: any, n: any) {
|
||||
const targets: string[] = n.notify_targets || n.notifyTargets || []
|
||||
if (targets.length) {
|
||||
const types: number[] = []
|
||||
const map: Record<number, number[]> = {}
|
||||
targets.forEach((s) => {
|
||||
const [tStr, idStr] = String(s).split(':')
|
||||
const t = Number(tStr)
|
||||
if (!t) return
|
||||
if (!map[t]) {
|
||||
map[t] = []
|
||||
types.push(t)
|
||||
}
|
||||
const id = Number(idStr)
|
||||
if (id > 0) map[t].push(id)
|
||||
})
|
||||
data.notifyTargets = targets.map(String)
|
||||
data.notifyTypes = types
|
||||
data.notifyIdsMap = map
|
||||
data.approverType = types[0] || data.approverType
|
||||
} else {
|
||||
const t = data.approverType
|
||||
const ids: number[] = [...(data.approverIds || [])]
|
||||
data.notifyTypes = [t]
|
||||
data.notifyIdsMap = ids.length ? { [t]: ids } : {}
|
||||
data.notifyTargets = ids.length ? ids.map((id: number) => `${t}:${id}`) : [`${t}:0`]
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化 / 外部切换流程时重建
|
||||
function reload() {
|
||||
const { ns, es } = buildGraph(props.modelValue || {})
|
||||
@@ -378,6 +438,9 @@ const nodeForm = reactive<any>({
|
||||
multiMode: 1,
|
||||
allowReturn: 0,
|
||||
timeoutHours: 48,
|
||||
// 通知节点多类型:选中的类型 + 各类型对应的对象 ID
|
||||
notifyTypes: [] as number[],
|
||||
notifyIdsMap: {} as Record<number, number[]>,
|
||||
})
|
||||
|
||||
const nodeDialogTitle = computed(() => {
|
||||
@@ -395,11 +458,28 @@ const currentPool = computed(() => {
|
||||
return meta ? (props.pools as any)[meta.poolKey] || [] : []
|
||||
})
|
||||
|
||||
const poolOf = (poolKey: string) => (props.pools as any)[poolKey] || []
|
||||
|
||||
// 通知节点:已选类型中需要手动指定对象的元信息
|
||||
const selectedNotifyMetas = computed(() =>
|
||||
(props.approverTypes as any[]).filter(
|
||||
(t) => (nodeForm.notifyTypes || []).includes(t.value) && t.needsSelection
|
||||
)
|
||||
)
|
||||
|
||||
function onNotifyTypesChange(vals: number[]) {
|
||||
const keep = new Set<number>(vals)
|
||||
Object.keys(nodeForm.notifyIdsMap).forEach((k) => {
|
||||
if (!keep.has(Number(k))) delete nodeForm.notifyIdsMap[k]
|
||||
})
|
||||
}
|
||||
|
||||
function onNodeDblClick(evt: any) {
|
||||
const node = evt.node
|
||||
if (!node) return
|
||||
editingNodeId.value = node.id
|
||||
const d = node.data
|
||||
const isNotify = d.nodeType === NODE_TYPE.NOTIFY
|
||||
Object.assign(nodeForm, {
|
||||
name: d.name,
|
||||
nodeType: d.nodeType,
|
||||
@@ -408,6 +488,8 @@ function onNodeDblClick(evt: any) {
|
||||
multiMode: d.multiMode,
|
||||
allowReturn: d.allowReturn,
|
||||
timeoutHours: d.timeoutHours,
|
||||
notifyTypes: isNotify ? [...(d.notifyTypes || [d.approverType])] : [],
|
||||
notifyIdsMap: isNotify ? JSON.parse(JSON.stringify(d.notifyIdsMap || {})) : {},
|
||||
})
|
||||
nodeDialog.value = true
|
||||
}
|
||||
@@ -426,9 +508,44 @@ function confirmNode() {
|
||||
ElMessage.warning('请填写节点名称')
|
||||
return
|
||||
}
|
||||
// 通知节点:多类型 + 各类型对象,归一化为 "类型:ID" 列表
|
||||
if (nodeForm.nodeType === NODE_TYPE.NOTIFY) {
|
||||
if (!nodeForm.notifyTypes.length) {
|
||||
ElMessage.warning('请选择通知类型')
|
||||
return
|
||||
}
|
||||
for (const t of nodeForm.notifyTypes) {
|
||||
const m = (props.approverTypes as any[]).find((x) => x.value === t)
|
||||
if (m?.needsSelection && !(nodeForm.notifyIdsMap[t] || []).length) {
|
||||
ElMessage.warning(`请选择「${m.label}」的通知对象`)
|
||||
return
|
||||
}
|
||||
}
|
||||
const targets: string[] = []
|
||||
nodeForm.notifyTypes.forEach((t: number) => {
|
||||
const ids = nodeForm.notifyIdsMap[t] || []
|
||||
if (ids.length) {
|
||||
ids.forEach((id: number) => targets.push(`${t}:${id}`))
|
||||
} else {
|
||||
targets.push(`${t}:0`)
|
||||
}
|
||||
})
|
||||
node.data = makeNodeData({
|
||||
node_type: NODE_TYPE.NOTIFY,
|
||||
node_name: nodeForm.name,
|
||||
notify_targets: targets,
|
||||
approver_type: nodeForm.notifyTypes[0],
|
||||
approver_ids: [],
|
||||
multi_mode: nodeForm.multiMode,
|
||||
allow_return: nodeForm.allowReturn,
|
||||
timeout_hours: nodeForm.timeoutHours,
|
||||
})
|
||||
nodeDialog.value = false
|
||||
return
|
||||
}
|
||||
const meta = currentApproverMeta.value
|
||||
if ((nodeForm.nodeType === NODE_TYPE.APPROVAL || nodeForm.nodeType === NODE_TYPE.NOTIFY) && meta && meta.needsSelection && !(nodeForm.approverIds || []).length) {
|
||||
ElMessage.warning(nodeForm.nodeType === NODE_TYPE.NOTIFY ? '请选择通知对象' : '请选择审批人')
|
||||
if (nodeForm.nodeType === NODE_TYPE.APPROVAL && meta && meta.needsSelection && !(nodeForm.approverIds || []).length) {
|
||||
ElMessage.warning('请选择审批人')
|
||||
return
|
||||
}
|
||||
node.data = makeNodeData({
|
||||
@@ -481,19 +598,25 @@ function removeCurrentEdge() {
|
||||
|
||||
// ==================== 对外序列化 ====================
|
||||
function serialize() {
|
||||
const outNodes = nodes.value.map((n: any, i: number) => ({
|
||||
node_key: n.id,
|
||||
node_name: n.data.name,
|
||||
node_type: n.data.nodeType,
|
||||
approver_type: n.data.approverType,
|
||||
approver_ids: n.data.approverIds,
|
||||
multi_mode: n.data.multiMode,
|
||||
allow_return: n.data.allowReturn,
|
||||
timeout_hours: n.data.timeoutHours,
|
||||
sort: i + 1,
|
||||
pos_x: Math.round(n.position.x),
|
||||
pos_y: Math.round(n.position.y),
|
||||
}))
|
||||
const outNodes = nodes.value.map((n: any, i: number) => {
|
||||
const isNotify = n.data.nodeType === NODE_TYPE.NOTIFY
|
||||
return {
|
||||
node_key: n.id,
|
||||
node_name: n.data.name,
|
||||
node_type: n.data.nodeType,
|
||||
approver_type: isNotify
|
||||
? (n.data.notifyTypes?.length ? n.data.notifyTypes[0] : n.data.approverType)
|
||||
: n.data.approverType,
|
||||
approver_ids: isNotify ? [] : n.data.approverIds,
|
||||
notify_targets: isNotify ? n.data.notifyTargets || [] : [],
|
||||
multi_mode: n.data.multiMode,
|
||||
allow_return: n.data.allowReturn,
|
||||
timeout_hours: n.data.timeoutHours,
|
||||
sort: i + 1,
|
||||
pos_x: Math.round(n.position.x),
|
||||
pos_y: Math.round(n.position.y),
|
||||
}
|
||||
})
|
||||
const outEdges = edges.value.map((e: any) => ({
|
||||
from: e.source,
|
||||
to: e.target,
|
||||
@@ -841,4 +964,15 @@ defineExpose({ serialize, fitView })
|
||||
margin-bottom: 4px;
|
||||
color: #ffd04b;
|
||||
}
|
||||
|
||||
// 节点弹窗底部:删除靠左,取消/确定靠右
|
||||
.node-dialog-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.footer-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>加入申请</h2>
|
||||
<div class="header-actions">
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<div class="filter-bar">
|
||||
<el-form :inline="true" :model="filterForm" class="filter-form">
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="filterForm.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="width: 140px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="待审核" value="0" />
|
||||
<el-option label="已通过" value="1" />
|
||||
<el-option label="已驳回" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 申请列表 -->
|
||||
<el-table :data="applies" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||
<el-table-column label="申请人" min-width="140" align="center">
|
||||
<template #default="scope">
|
||||
<div>{{ scope.row.identity_nickname || "-" }}</div>
|
||||
<div class="sub-text">{{ scope.row.identity_mobile || "" }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="tenant_name"
|
||||
label="申请加入企业"
|
||||
min-width="160"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column label="联系方式" min-width="150" align="center">
|
||||
<template #default="scope">
|
||||
<div>{{ scope.row.contact_person || "-" }}</div>
|
||||
<div class="sub-text">{{ scope.row.contact_phone || "" }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusTagType(scope.row.status)" size="small">
|
||||
{{ statusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="驳回原因" min-width="140" align="center">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.status === 2">{{
|
||||
scope.row.reject_reason || "-"
|
||||
}}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="reviewer_name"
|
||||
label="审核人"
|
||||
width="110"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ scope.row.reviewer_name || "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核时间" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.review_time ? formatDateTime(row.review_time) : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="申请时间" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
align="center"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.status === 0">
|
||||
<el-button
|
||||
size="small"
|
||||
type="success"
|
||||
link
|
||||
@click="handleApprove(scope.row)"
|
||||
>
|
||||
通过
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleReject(scope.row)"
|
||||
>
|
||||
驳回
|
||||
</el-button>
|
||||
</template>
|
||||
<span v-else class="sub-text">已处理</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Refresh, Search } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getTenantApplies,
|
||||
approveTenantApply,
|
||||
rejectTenantApply,
|
||||
} from "@/api/tenantApply";
|
||||
import { formatDateTime } from "@/utils/datetime";
|
||||
|
||||
interface TenantApply {
|
||||
id: number;
|
||||
identity_id: number;
|
||||
tenant_name: string;
|
||||
identity_nickname: string;
|
||||
identity_mobile: string;
|
||||
contact_person: string;
|
||||
contact_phone: string;
|
||||
contact_email: string;
|
||||
status: number;
|
||||
reject_reason: string;
|
||||
reviewer_name: string;
|
||||
review_time: string | null;
|
||||
create_time: string;
|
||||
}
|
||||
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const applies = ref<TenantApply[]>([]);
|
||||
|
||||
const filterForm = ref({
|
||||
status: "",
|
||||
});
|
||||
|
||||
const statusText = (status: number): string => {
|
||||
const map: Record<number, string> = {
|
||||
0: "待审核",
|
||||
1: "已通过",
|
||||
2: "已驳回",
|
||||
};
|
||||
return map[status] || "未知";
|
||||
};
|
||||
|
||||
const statusTagType = (status: number): string => {
|
||||
const map: Record<number, string> = {
|
||||
0: "warning",
|
||||
1: "success",
|
||||
2: "danger",
|
||||
};
|
||||
return map[status] || "info";
|
||||
};
|
||||
|
||||
// 获取申请列表
|
||||
const fetchApplies = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: any = {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
};
|
||||
if (filterForm.value.status !== "") {
|
||||
params.status = filterForm.value.status;
|
||||
}
|
||||
const res = await getTenantApplies(params);
|
||||
if (res.code === 200) {
|
||||
applies.value = res.data.list || [];
|
||||
total.value = res.data.total || 0;
|
||||
} else {
|
||||
ElMessage.error(res.msg || "获取申请列表失败");
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || "获取申请列表失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
fetchApplies();
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
page.value = 1;
|
||||
fetchApplies();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
filterForm.value = { status: "" };
|
||||
page.value = 1;
|
||||
fetchApplies();
|
||||
};
|
||||
|
||||
const handleSizeChange = () => {
|
||||
page.value = 1;
|
||||
fetchApplies();
|
||||
};
|
||||
|
||||
const handlePageChange = () => {
|
||||
fetchApplies();
|
||||
};
|
||||
|
||||
// 通过申请:申请人加入本企业
|
||||
const handleApprove = async (row: TenantApply) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认通过「${row.identity_nickname || row.identity_mobile}」加入本企业的申请?通过后将默认分配「租户用户」角色。`,
|
||||
"通过申请",
|
||||
{ confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" },
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await approveTenantApply(row.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(res.msg || "已通过");
|
||||
fetchApplies();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "操作失败");
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || "操作失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 驳回申请(需填写原因)
|
||||
const handleReject = async (row: TenantApply) => {
|
||||
let reason = "";
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt(
|
||||
"请输入驳回原因",
|
||||
`驳回「${row.identity_nickname || row.identity_mobile}」的申请`,
|
||||
{
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
inputPlaceholder: "驳回原因(可选)",
|
||||
inputValidator: (v: string) => {
|
||||
if (v && v.length > 200) return "原因不能超过200字";
|
||||
return true;
|
||||
},
|
||||
},
|
||||
);
|
||||
reason = value || "";
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await rejectTenantApply(row.id, { reason });
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(res.msg || "已驳回");
|
||||
fetchApplies();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "操作失败");
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || "操作失败");
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchApplies();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
|
||||
.filter-form {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-bar {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
color: #8a94ad;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
@@ -161,7 +161,8 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
// 调用接口修改密码
|
||||
const res = await changePassword(form.value.id, passwordForm.value);
|
||||
// 第三个参数 true:userId 是用户管理列表行主键,后端按主键精确匹配
|
||||
const res = await changePassword(form.value.id, passwordForm.value, true);
|
||||
|
||||
if (res.code === 200 || res.msg === '修改成功') {
|
||||
ElMessage.success("密码修改成功");
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { addUser, editUser, getUserInfo } from "@/api/user";
|
||||
import { getAllRoles } from "@/api/role";
|
||||
|
||||
@@ -161,11 +161,25 @@ const fetchRoles = async () => {
|
||||
try {
|
||||
const res = await getAllRoles();
|
||||
roles.value = (res.data || []).filter((r: RoleOption) => r.status !== 0);
|
||||
applyDefaultRole();
|
||||
} catch (e) {
|
||||
roles.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 新增用户时,角色默认选中全局「租户用户」角色(tenant_id=0,平台统一维护)
|
||||
const applyDefaultRole = () => {
|
||||
if (!isAdd.value || form.value.group_id) {
|
||||
return;
|
||||
}
|
||||
const def = roles.value.find(
|
||||
(r) => r.name === "租户用户" && Number(r.tenant_id || 0) === 0
|
||||
);
|
||||
if (def) {
|
||||
form.value.group_id = def.id;
|
||||
}
|
||||
};
|
||||
|
||||
// 平台规定的默认角色(全局角色,tenant_id=0,平台端统一维护)
|
||||
const globalRoles = computed(() =>
|
||||
roles.value.filter((r) => Number(r.tenant_id || 0) === 0)
|
||||
@@ -273,7 +287,9 @@ const loadUserData = async (user: any) => {
|
||||
throw new Error("未提供有效的用户 ID");
|
||||
}
|
||||
|
||||
const res = await getUserInfo(userId);
|
||||
// 列表行的 id 是 yz_system_tenant_user 主键,必须 by=id 精确匹配,
|
||||
// 否则后端会先当 identity_id 换算老 uid,命中别人的记录
|
||||
const res = await getUserInfo(userId, { by: "id" });
|
||||
|
||||
const data = res.data || res;
|
||||
|
||||
@@ -362,8 +378,53 @@ const handleSubmit = async () => {
|
||||
group_id: form.value.group_id,
|
||||
};
|
||||
|
||||
await addUser(submitData);
|
||||
ElMessage.success("添加成功");
|
||||
const res: any = await addUser(submitData);
|
||||
|
||||
// 统一认证:账号在别的企业已存在(后端不覆盖),需验证原身份密码后绑定
|
||||
if (res?.code === 409 && res?.data?.need_verify) {
|
||||
const tenantNames = (res.data.tenants || [])
|
||||
.map((t: any) => t.tenant_name)
|
||||
.filter(Boolean)
|
||||
.join("、");
|
||||
const tip = tenantNames
|
||||
? `该账号已在「${tenantNames}」创建过账号。请输入该账号的登录密码验证身份,验证通过后直接绑定本企业(不会修改原密码)`
|
||||
: "该账号已在其他企业存在。请输入该账号的登录密码验证身份,验证通过后直接绑定本企业(不会修改原密码)";
|
||||
|
||||
// 密码错误时重新弹窗让用户重试,取消则停留在表单
|
||||
for (;;) {
|
||||
let verifyPassword = "";
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt(tip, "身份验证", {
|
||||
confirmButtonText: "验证并绑定",
|
||||
cancelButtonText: "取消",
|
||||
inputType: "password",
|
||||
inputPlaceholder: "请输入该账号的登录密码",
|
||||
inputValidator: (v: string) =>
|
||||
v && v.trim() ? true : "密码不能为空",
|
||||
});
|
||||
verifyPassword = value;
|
||||
} catch {
|
||||
return; // 用户取消验证
|
||||
}
|
||||
|
||||
// 带 identity_id + verify_password 重新提交,后端验证通过只建绑定
|
||||
const retry: any = await addUser({
|
||||
...submitData,
|
||||
identity_id: res.data.identity_id,
|
||||
verify_password: verifyPassword,
|
||||
});
|
||||
|
||||
if (retry?.code === 200) {
|
||||
ElMessage.success("身份验证通过,已绑定本企业");
|
||||
break;
|
||||
}
|
||||
ElMessage.error(retry?.msg || "验证失败,请重新输入密码");
|
||||
}
|
||||
} else if (res?.code === 200) {
|
||||
ElMessage.success("添加成功");
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "添加失败");
|
||||
}
|
||||
} else {
|
||||
// 编辑用户
|
||||
if (!form.value.id || form.value.id === 0) {
|
||||
@@ -388,7 +449,7 @@ const handleSubmit = async () => {
|
||||
submitData.password = form.value.password;
|
||||
}
|
||||
|
||||
await editUser(form.value.id, submitData);
|
||||
await editUser(form.value.id, submitData, { by: "id" });
|
||||
ElMessage.success("更新成功");
|
||||
}
|
||||
|
||||
@@ -423,6 +484,7 @@ defineExpose({
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
applyDefaultRole();
|
||||
},
|
||||
openEdit: (user: any) => {
|
||||
isAdd.value = false;
|
||||
@@ -451,7 +513,9 @@ defineExpose({
|
||||
confirmPassword: "",
|
||||
email: "",
|
||||
status: 1,
|
||||
group_id: undefined,
|
||||
};
|
||||
applyDefaultRole();
|
||||
}
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
|
||||
@@ -306,7 +306,7 @@ const handleDelete = async (user: User) => {
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteUser(user.id);
|
||||
await deleteUser(user.id, { by: "id" });
|
||||
ElMessage.success("删除成功");
|
||||
fetchUsers();
|
||||
fetchQuotaInfo();
|
||||
|
||||
Reference in new issue
Block a user