20260730231110

This commit is contained in:
oneao committed 2026-07-30 23:11:10 +08:00
1 parent 4929b78434
commit 318fa0d9e1
23 files changed
+4026 -506

No files matched your search

@@ -1,10 +1,429 @@
<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 data-code="b_user" />
<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>
@@ -22,4 +441,208 @@ import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vu
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>