diff --git a/backend/src/api/tenantApply.js b/backend/src/api/tenantApply.js new file mode 100644 index 0000000..337cc04 --- /dev/null +++ b/backend/src/api/tenantApply.js @@ -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, + }); +} diff --git a/backend/src/api/user.js b/backend/src/api/user.js index df701ca..6da15ef 100644 --- a/backend/src/api/user.js +++ b/backend/src/api/user.js @@ -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, }, }); } diff --git a/backend/src/components/approval/FlowCanvas.vue b/backend/src/components/approval/FlowCanvas.vue index a69c774..95fcd32 100644 --- a/backend/src/components/approval/FlowCanvas.vue +++ b/backend/src/components/approval/FlowCanvas.vue @@ -144,29 +144,43 @@ 判断节点:拖出多条连线,点击连线分别填写分支条件(留空为默认分支)。运行期按条件命中其中一条分支。 @@ -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 = {} + 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({ multiMode: 1, allowReturn: 0, timeoutHours: 48, + // 通知节点多类型:选中的类型 + 各类型对应的对象 ID + notifyTypes: [] as number[], + notifyIdsMap: {} as Record, }) 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(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; + } +} diff --git a/backend/src/views/basicSettings/applies/index.vue b/backend/src/views/basicSettings/applies/index.vue new file mode 100644 index 0000000..2eb4751 --- /dev/null +++ b/backend/src/views/basicSettings/applies/index.vue @@ -0,0 +1,353 @@ + + + + + diff --git a/backend/src/views/basicSettings/users/components/changePassword.vue b/backend/src/views/basicSettings/users/components/changePassword.vue index 8b64697..563edb2 100644 --- a/backend/src/views/basicSettings/users/components/changePassword.vue +++ b/backend/src/views/basicSettings/users/components/changePassword.vue @@ -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("密码修改成功"); diff --git a/backend/src/views/basicSettings/users/components/userEdit.vue b/backend/src/views/basicSettings/users/components/userEdit.vue index 2be9410..5476266 100644 --- a/backend/src/views/basicSettings/users/components/userEdit.vue +++ b/backend/src/views/basicSettings/users/components/userEdit.vue @@ -109,7 +109,7 @@ + + diff --git a/go/views/auth/login.tpl b/go/views/auth/login.tpl index 9eae823..a4418d0 100644 --- a/go/views/auth/login.tpl +++ b/go/views/auth/login.tpl @@ -164,6 +164,17 @@ 75% { transform: translateX(4px); } } + .notice-box { + display: flex; align-items: flex-start; gap: 9px; + background: #f0fff4; border: 1px solid #b7ebc2; color: #1f7a37; + padding: 11px 14px; border-radius: 10px; font-size: 13px; line-height: 1.6; + margin-bottom: 18px; + } + .notice-box.warn { + background: #fffbe6; border-color: #ffe89e; color: #a96a00; + } + .notice-box svg { width: 16px; height: 16px; flex: 0 0 auto; margin-top: 2px; } + .field { margin-bottom: 18px; } .field-label { display: flex; align-items: center; justify-content: space-between; @@ -455,6 +466,17 @@

请输入您的账号与密码以继续

+ {{if .Notice}} +
+ + {{if eq .Notice "registered_warn"}}{{else}}{{end}} + + + {{if eq .Notice "reset"}}密码已重置,请使用新密码登录{{else if eq .Notice "registered_warn"}}{{.WarnMsg}},可先登录{{else}}注册成功,请使用新账号登录{{end}} + +
+ {{end}} +
@@ -493,6 +515,7 @@
+ 忘记密码?
+
还没有账号?立即注册
+ @@ -661,8 +636,6 @@ import { formatTime } from "@/utils/datetime"; import { getWechatMpConfig, saveWechatMpConfig, - getWechatMiniConfig, - saveWechatMiniConfig, testWechatMpConnection, publishWechatMpMenu, uploadWechatFollowQrcode, @@ -678,6 +651,7 @@ import { pushWechatMpMessage, pushWechatMpTest } from "@/api/wechatMp"; +import miniAppSettings from "./miniAppSettings.vue"; const STORAGE_KEY = "notification_settings_draft"; const activeSubTab = ref("email"); @@ -1089,56 +1063,6 @@ const saveWechatConfig = async () => { } }; -/* ==================== 微信小程序(一键登录) ==================== */ - -const miniCfg = reactive({ - app_id: "", - app_secret: "", - enabled: 0, - auto_bind: 1, - remark: "", - configured: false, -}); -const miniSaving = ref(false); - -const loadMiniConfig = async () => { - try { - const res = await getWechatMiniConfig(); - if (res.code === 200 && res.data) { - Object.assign(miniCfg, res.data); - } - } catch (e) { - console.error("加载小程序配置失败:", e); - } -}; - -const saveMiniConfig = async () => { - if (!miniCfg.app_id) { - ElMessage.warning("请先填写 AppID"); - return; - } - miniSaving.value = true; - try { - const res = await saveWechatMiniConfig({ - app_id: miniCfg.app_id, - app_secret: miniCfg.app_secret, - enabled: Number(miniCfg.enabled) === 1 ? 1 : 0, - auto_bind: Number(miniCfg.auto_bind) === 1 ? 1 : 0, - remark: miniCfg.remark, - }); - if (res.code === 200) { - ElMessage.success("小程序配置保存成功"); - await loadMiniConfig(); - } else { - ElMessage.error(res.msg || "保存失败"); - } - } catch (e) { - ElMessage.error(e?.message || "保存失败"); - } finally { - miniSaving.value = false; - } -}; - // 关注二维码上传/删除(存 data URL,供租户端「绑定微信」页展示扫码) const followQrcodeUploading = ref(false); const handleFollowQrcodeUpload = async ({ file }) => { @@ -1434,7 +1358,6 @@ onMounted(() => { loadSiteReminderConfig(); loadBarkConfig(); loadWechatConfig(); - loadMiniConfig(); loadWechatBinding(); }); @@ -1442,7 +1365,6 @@ onMounted(() => { watch(activeSubTab, (tab) => { if (tab === "wechat") { loadWechatConfig(); - loadMiniConfig(); loadWechatBinding(); loadFollowers(); } diff --git a/platform/src/views/tenantApply/index.vue b/platform/src/views/tenantApply/index.vue new file mode 100644 index 0000000..7640a4c --- /dev/null +++ b/platform/src/views/tenantApply/index.vue @@ -0,0 +1,424 @@ + + + + +