Files
workspace/code/fms/fms-vue/src/views/system/user/index.vue
T
2026-07-30 23:11:10 +08:00

649 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>