649 lines
18 KiB
Vue
649 lines
18 KiB
Vue
<script setup>
|
||
import { ref, reactive, computed } from "vue";
|
||
import { message } from "antdv-next";
|
||
import { Plus, ChevronRight, ChevronDown } from "@lucide/vue";
|
||
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
|
||
|
||
const listRef = ref(null);
|
||
|
||
// ── Mock data ──
|
||
const mockUser = {
|
||
b_id: "1001",
|
||
b_account: "zhangsan",
|
||
b_name: "张三",
|
||
b_phone: "13800138000",
|
||
b_department: "技术部",
|
||
b_canuse: "1",
|
||
b_salesman: "0",
|
||
b_docman: "0",
|
||
b_dealman: "1",
|
||
b_othermachinelogin: "0",
|
||
b_visible_customer: "1",
|
||
b_ismanager: "0",
|
||
b_purview: "1",
|
||
b_spview: "0",
|
||
b_subcompany: "",
|
||
b_inputuser: "admin",
|
||
b_inputdatetime: "2025-01-15 10:30:00",
|
||
};
|
||
|
||
// 模块+权限合并的树数据:每个模块挂载自己的 power 列表
|
||
const mockModuleTree = [
|
||
{
|
||
b_id: "g1",
|
||
b_name: "基础资料",
|
||
children: [
|
||
{
|
||
b_id: "m11", b_code: "m_customer", b_name: "客户管理",
|
||
powers: ["查看", "新增", "修改", "删除", "导出"],
|
||
},
|
||
{
|
||
b_id: "m12", b_code: "m_supplier", b_name: "供应商管理",
|
||
powers: ["查看", "新增", "修改", "删除"],
|
||
},
|
||
{
|
||
b_id: "m13", b_code: "m_product", b_name: "产品管理",
|
||
powers: ["查看", "新增", "修改"],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
b_id: "g2",
|
||
b_name: "业务操作",
|
||
children: [
|
||
{
|
||
b_id: "m21", b_code: "m_order", b_name: "订单管理",
|
||
powers: ["查看", "新增", "修改", "删除", "导出", "审核"],
|
||
},
|
||
{
|
||
b_id: "m22", b_code: "m_ship", b_name: "发货管理",
|
||
powers: ["查看", "新增", "发货", "确认"],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
b_id: "g3",
|
||
b_name: "报表与分析",
|
||
children: [
|
||
{
|
||
b_id: "m31", b_code: "m_sales_report", b_name: "销售报表",
|
||
powers: ["查看", "导出"],
|
||
},
|
||
{
|
||
b_id: "m32", b_code: "m_stock_report", b_name: "库存报表",
|
||
powers: ["查看"],
|
||
},
|
||
],
|
||
},
|
||
];
|
||
|
||
// 该用户当前的全部权限状态:模块授权 + 每个模块的操作权限
|
||
// key = moduleId, value = Set of power names(含 "module" 表示模块已授权)
|
||
const mockUserGrants = {
|
||
m11: new Set(["module", "查看", "新增", "修改"]),
|
||
m13: new Set(["module", "查看"]),
|
||
m21: new Set(["module", "查看", "新增", "修改", "审核"]),
|
||
};
|
||
|
||
// ── Modal state ──
|
||
const modalVisible = ref(false);
|
||
const modalTitle = ref("新增用户");
|
||
const saving = ref(false);
|
||
const activeTab = ref("basic");
|
||
|
||
// Form data
|
||
const formData = reactive({
|
||
b_account: "",
|
||
b_name: "",
|
||
b_phone: "",
|
||
b_department: "",
|
||
b_canuse: true,
|
||
b_salesman: false,
|
||
b_docman: false,
|
||
b_dealman: false,
|
||
b_othermachinelogin: false,
|
||
b_visible_customer: false,
|
||
b_ismanager: false,
|
||
b_purview: true,
|
||
b_spview: false,
|
||
});
|
||
const password = ref("");
|
||
const passwordConfirm = ref("");
|
||
|
||
// 合并后的权限状态:{ [moduleId]: Set<powerName> },"module" 表示已授权该模块
|
||
const moduleGrants = ref({});
|
||
|
||
// 左侧菜单树:当前选中模块及分组展开状态
|
||
const selectedModId = ref(null);
|
||
const collapsedGroups = ref(new Set(["g3"])); // 默认折叠"报表与分析"
|
||
const selectedModule = computed(() => {
|
||
if (!selectedModId.value) return null;
|
||
for (const group of mockModuleTree) {
|
||
const found = group.children.find((m) => m.b_id === selectedModId.value);
|
||
if (found) return found;
|
||
}
|
||
return null;
|
||
});
|
||
function selectModule(modId) {
|
||
selectedModId.value = modId;
|
||
}
|
||
function toggleGroupExpand(gid) {
|
||
const next = new Set(collapsedGroups.value);
|
||
if (next.has(gid)) next.delete(gid);
|
||
else next.add(gid);
|
||
collapsedGroups.value = next;
|
||
}
|
||
|
||
function isModuleGranted(modId) {
|
||
return moduleGrants.value[modId]?.has("module") ?? false;
|
||
}
|
||
function isPowerGranted(modId, powerName) {
|
||
return moduleGrants.value[modId]?.has(powerName) ?? false;
|
||
}
|
||
function toggleModuleGrant(modId) {
|
||
const next = { ...moduleGrants.value };
|
||
if (isModuleGranted(modId)) {
|
||
// 取消模块授权 → 清除该模块所有权限
|
||
delete next[modId];
|
||
} else {
|
||
next[modId] = new Set(["module"]);
|
||
}
|
||
moduleGrants.value = next;
|
||
}
|
||
function togglePower(modId, powerName) {
|
||
const next = { ...moduleGrants.value };
|
||
const current = next[modId];
|
||
if (!current) return;
|
||
const nextSet = new Set(current);
|
||
if (nextSet.has(powerName)) nextSet.delete(powerName);
|
||
else nextSet.add(powerName);
|
||
next[modId] = nextSet;
|
||
moduleGrants.value = next;
|
||
}
|
||
function toggleAllPowers(mod) {
|
||
const modId = mod.b_id;
|
||
const current = moduleGrants.value[modId];
|
||
if (!current) return;
|
||
const next = { ...moduleGrants.value };
|
||
const nextSet = new Set(current);
|
||
const allGranted = mod.powers.every((p) => nextSet.has(p));
|
||
if (allGranted) {
|
||
for (const p of mod.powers) nextSet.delete(p);
|
||
} else {
|
||
for (const p of mod.powers) nextSet.add(p);
|
||
}
|
||
next[modId] = nextSet;
|
||
moduleGrants.value = next;
|
||
}
|
||
|
||
|
||
|
||
function resetForm() {
|
||
formData.b_account = "";
|
||
formData.b_name = "";
|
||
formData.b_phone = "";
|
||
formData.b_department = "";
|
||
formData.b_canuse = true;
|
||
formData.b_salesman = false;
|
||
formData.b_docman = false;
|
||
formData.b_dealman = false;
|
||
formData.b_othermachinelogin = false;
|
||
formData.b_visible_customer = false;
|
||
formData.b_ismanager = false;
|
||
formData.b_purview = true;
|
||
formData.b_spview = false;
|
||
password.value = "";
|
||
passwordConfirm.value = "";
|
||
moduleGrants.value = {};
|
||
selectedModId.value = null;
|
||
collapsedGroups.value = new Set(["g3"]);
|
||
}
|
||
|
||
function fillEditForm(user) {
|
||
formData.b_account = user.b_account || "";
|
||
formData.b_name = user.b_name || "";
|
||
formData.b_phone = user.b_phone || "";
|
||
formData.b_department = user.b_department || "";
|
||
formData.b_canuse = user.b_canuse === "1" || user.b_canuse === 1;
|
||
formData.b_salesman = user.b_salesman === "1" || user.b_salesman === 1;
|
||
formData.b_docman = user.b_docman === "1" || user.b_docman === 1;
|
||
formData.b_dealman = user.b_dealman === "1" || user.b_dealman === 1;
|
||
formData.b_othermachinelogin = user.b_othermachinelogin === "1" || user.b_othermachinelogin === 1;
|
||
formData.b_visible_customer = user.b_visible_customer === "1" || user.b_visible_customer === 1;
|
||
formData.b_ismanager = user.b_ismanager === "1" || user.b_ismanager === 1;
|
||
formData.b_purview = user.b_purview === "1" || user.b_purview === 1;
|
||
formData.b_spview = user.b_spview === "1" || user.b_spview === 1;
|
||
password.value = "";
|
||
passwordConfirm.value = "";
|
||
}
|
||
|
||
// ── Handlers ──
|
||
function handleAdd() {
|
||
modalTitle.value = "新增用户";
|
||
activeTab.value = "basic";
|
||
resetForm();
|
||
modalVisible.value = true;
|
||
}
|
||
|
||
function handleEdit(row) {
|
||
modalTitle.value = `编辑用户 - ${row.b_name || row.b_account || ""}`;
|
||
activeTab.value = "basic";
|
||
fillEditForm(mockUser);
|
||
// 深拷贝 mock user grants
|
||
const grants = {};
|
||
for (const [modId, powers] of Object.entries(mockUserGrants)) {
|
||
grants[modId] = new Set(powers);
|
||
}
|
||
moduleGrants.value = grants;
|
||
selectedModId.value = mockModuleTree[0].children[0].b_id;
|
||
collapsedGroups.value = new Set();
|
||
modalVisible.value = true;
|
||
}
|
||
|
||
const rowActions = [
|
||
{ key: "edit", type: "action", label: "编辑", onClick: handleEdit },
|
||
];
|
||
|
||
// Save (demo only)
|
||
async function handleSave() {
|
||
saving.value = true;
|
||
await new Promise((r) => setTimeout(r, 600));
|
||
saving.value = false;
|
||
modalVisible.value = false;
|
||
message.success("(Demo)保存成功 — 未真实调用接口");
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="user-page">
|
||
<FmsModuleListPage
|
||
ref="listRef"
|
||
data-code="b_user"
|
||
:row-actions="rowActions"
|
||
@row-action="({ record }) => { if (record?.action?.key === 'edit') handleEdit(record.record) }"
|
||
>
|
||
<template #actions>
|
||
<a-button type="primary" @click="handleAdd">
|
||
<Plus :size="15" />
|
||
新增用户
|
||
</a-button>
|
||
</template>
|
||
</FmsModuleListPage>
|
||
|
||
<!-- Demo Edit Modal -->
|
||
<a-modal
|
||
v-model:open="modalVisible"
|
||
:title="modalTitle"
|
||
:width="1000"
|
||
:confirm-loading="saving"
|
||
:body-style="{ padding: '0 24px 16px', maxHeight: '70vh', overflow: 'auto' }"
|
||
@ok="handleSave"
|
||
@cancel="modalVisible = false"
|
||
>
|
||
<a-tabs v-model:active-key="activeTab">
|
||
<!-- Tab 1: 基本信息(含密码) -->
|
||
<a-tab-pane key="basic" tab="基本信息">
|
||
<div class="demo-basic-info">
|
||
<a-row :gutter="[24, 16]">
|
||
<a-col :span="12">
|
||
<label class="demo-field-label">登录账号 <span class="demo-required">*</span></label>
|
||
<a-input v-model:value="formData.b_account" placeholder="请输入登录账号" />
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<label class="demo-field-label">用户姓名 <span class="demo-required">*</span></label>
|
||
<a-input v-model:value="formData.b_name" placeholder="请输入用户姓名" />
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<label class="demo-field-label">手机号</label>
|
||
<a-input v-model:value="formData.b_phone" placeholder="请输入手机号" />
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<label class="demo-field-label">部门</label>
|
||
<a-input v-model:value="formData.b_department" placeholder="请输入部门" />
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<label class="demo-field-label">新密码</label>
|
||
<a-input-password v-model:value="password" placeholder="留空则不修改" />
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<label class="demo-field-label">确认密码</label>
|
||
<a-input-password v-model:value="passwordConfirm" placeholder="请再次输入" />
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<!-- 标志位 -->
|
||
<div class="demo-flags-card">
|
||
<div class="demo-card-title">标志位</div>
|
||
<a-row :gutter="[16, 12]">
|
||
<a-col :span="6">
|
||
<a-checkbox v-model:checked="formData.b_canuse">启用</a-checkbox>
|
||
</a-col>
|
||
<a-col :span="6">
|
||
<a-checkbox v-model:checked="formData.b_salesman">销售员</a-checkbox>
|
||
</a-col>
|
||
<a-col :span="6">
|
||
<a-checkbox v-model:checked="formData.b_docman">单据主管</a-checkbox>
|
||
</a-col>
|
||
<a-col :span="6">
|
||
<a-checkbox v-model:checked="formData.b_dealman">可处理事务</a-checkbox>
|
||
</a-col>
|
||
<a-col :span="6">
|
||
<a-checkbox v-model:checked="formData.b_othermachinelogin">允许异地登录</a-checkbox>
|
||
</a-col>
|
||
<a-col :span="6">
|
||
<a-checkbox v-model:checked="formData.b_visible_customer">可见客户</a-checkbox>
|
||
</a-col>
|
||
<a-col :span="6">
|
||
<a-checkbox v-model:checked="formData.b_ismanager">管理员</a-checkbox>
|
||
</a-col>
|
||
<a-col :span="6">
|
||
<a-checkbox v-model:checked="formData.b_purview">操作权限</a-checkbox>
|
||
</a-col>
|
||
<a-col :span="6">
|
||
<a-checkbox v-model:checked="formData.b_spview">审批权限</a-checkbox>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- Tab 2: 菜单权限(左树右操作列表) -->
|
||
<a-tab-pane key="module-power" tab="菜单权限">
|
||
<div class="perm-layout">
|
||
<!-- 左侧:菜单树 -->
|
||
<div class="perm-tree">
|
||
<div v-for="group in mockModuleTree" :key="group.b_id" class="tree-group">
|
||
<div class="tree-group-hd" @click="toggleGroupExpand(group.b_id)">
|
||
<span class="tree-group-ic">
|
||
<ChevronRight v-if="collapsedGroups.has(group.b_id)" :size="14" />
|
||
<ChevronDown v-else :size="14" />
|
||
</span>
|
||
<span class="tree-group-label">{{ group.b_name }}</span>
|
||
<span class="tree-group-count">{{ group.children.length }}</span>
|
||
</div>
|
||
<div v-show="!collapsedGroups.has(group.b_id)" class="tree-group-body">
|
||
<div
|
||
v-for="mod in group.children"
|
||
:key="mod.b_id"
|
||
class="tree-item"
|
||
:class="{ 'is-active': selectedModId === mod.b_id }"
|
||
@click="selectModule(mod.b_id)"
|
||
>
|
||
<a-checkbox
|
||
:checked="isModuleGranted(mod.b_id)"
|
||
@click.stop
|
||
@change="toggleModuleGrant(mod.b_id)"
|
||
/>
|
||
<span class="tree-item-name">{{ mod.b_name }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:操作权限列表 -->
|
||
<div class="perm-detail">
|
||
<template v-if="selectedModule">
|
||
<div class="perm-detail-hd">
|
||
<span class="perm-detail-title">{{ selectedModule.b_name }}</span>
|
||
<span class="perm-detail-sub">{{ selectedModule.b_code }}</span>
|
||
</div>
|
||
<div class="perm-detail-body">
|
||
<template v-if="isModuleGranted(selectedModule.b_id)">
|
||
<label class="perm-detail-all">
|
||
<a-checkbox
|
||
:checked="selectedModule.powers.every((p) => isPowerGranted(selectedModule.b_id, p))"
|
||
:indeterminate="selectedModule.powers.some((p) => isPowerGranted(selectedModule.b_id, p)) && !selectedModule.powers.every((p) => isPowerGranted(selectedModule.b_id, p))"
|
||
@change="toggleAllPowers(selectedModule)"
|
||
/>
|
||
<span>全选操作权限</span>
|
||
</label>
|
||
<div class="perm-pow-list">
|
||
<label
|
||
v-for="pow in selectedModule.powers"
|
||
:key="pow"
|
||
class="perm-pow-item"
|
||
>
|
||
<a-checkbox
|
||
:checked="isPowerGranted(selectedModule.b_id, pow)"
|
||
@change="togglePower(selectedModule.b_id, pow)"
|
||
/>
|
||
<span>{{ pow }}</span>
|
||
</label>
|
||
</div>
|
||
</template>
|
||
<div v-else class="perm-detail-tip">
|
||
勾选左侧「{{ selectedModule.b_name }}」以授权访问,再配置具体操作权限
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<div v-else class="perm-detail-empty">
|
||
请选择左侧菜单
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.user-page {
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
border: 1px solid var(--fms-border);
|
||
border-radius: 6px;
|
||
background: var(--fms-surface);
|
||
}
|
||
|
||
.user-page :deep(.fms-module-list-page) {
|
||
border: 0;
|
||
border-radius: 0;
|
||
}
|
||
|
||
// ── Modal internal styles ──
|
||
.demo-basic-info {
|
||
padding-top: 4px;
|
||
}
|
||
|
||
.demo-field-label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-size: 13px;
|
||
color: #333;
|
||
}
|
||
|
||
.demo-required {
|
||
color: #ff4d4f;
|
||
}
|
||
|
||
.demo-flags-card {
|
||
margin-top: 20px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.demo-card-title {
|
||
margin-bottom: 10px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #666;
|
||
}
|
||
|
||
// ── Menu permission: left tree + right list ──
|
||
.perm-layout {
|
||
display: flex;
|
||
height: 440px;
|
||
border: 1px solid #f0f0f0;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
// 左侧:菜单树
|
||
.perm-tree {
|
||
width: 240px;
|
||
min-width: 240px;
|
||
padding: 6px 0;
|
||
border-right: 1px solid #f0f0f0;
|
||
overflow-y: auto;
|
||
background: #fafbfc;
|
||
}
|
||
|
||
.tree-group-hd {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 12px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
transition: background 0.1s;
|
||
|
||
&:hover {
|
||
background: #f0f1f3;
|
||
}
|
||
}
|
||
|
||
.tree-group-ic {
|
||
display: flex;
|
||
align-items: center;
|
||
color: #999;
|
||
}
|
||
|
||
.tree-group-label {
|
||
flex: 1;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #1a1a1a;
|
||
}
|
||
|
||
.tree-group-count {
|
||
font-size: 11px;
|
||
color: #bbb;
|
||
}
|
||
|
||
.tree-group-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.tree-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 12px 8px 30px;
|
||
cursor: pointer;
|
||
transition: background 0.1s;
|
||
|
||
&:hover {
|
||
background: #ebedf0;
|
||
}
|
||
|
||
&.is-active {
|
||
background: #e6f0ff;
|
||
|
||
&:hover {
|
||
background: #dcebff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.tree-item-name {
|
||
font-size: 13px;
|
||
color: #333;
|
||
user-select: none;
|
||
}
|
||
|
||
// 右侧:操作权限列表
|
||
.perm-detail {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #fff;
|
||
}
|
||
|
||
.perm-detail-hd {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
padding: 14px 20px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.perm-detail-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #1a1a1a;
|
||
}
|
||
|
||
.perm-detail-sub {
|
||
font-size: 12px;
|
||
color: #bbb;
|
||
}
|
||
|
||
.perm-detail-body {
|
||
flex: 1;
|
||
padding: 12px 20px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.perm-detail-all {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
margin-bottom: 6px;
|
||
border-radius: 6px;
|
||
background: #f6f7f9;
|
||
font-size: 13px;
|
||
color: #333;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
.perm-pow-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.perm-pow-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 11px 12px;
|
||
border-bottom: 1px solid #f5f5f5;
|
||
font-size: 13px;
|
||
color: #333;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
|
||
&:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
&:hover {
|
||
background: #fafbfc;
|
||
}
|
||
}
|
||
|
||
.perm-detail-tip {
|
||
margin-top: 8px;
|
||
padding: 36px 16px;
|
||
text-align: center;
|
||
font-size: 13px;
|
||
color: #999;
|
||
background: #fafbfc;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.perm-detail-empty {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 13px;
|
||
color: #bbb;
|
||
}
|
||
</style>
|