This commit is contained in:
oneao committed 2026-03-20 16:58:54 +08:00
1 parent c330e52fd3
commit 87d63b6542
341 files changed
+128714 -48

No files matched your search

+1 -1
View File
@@ -10,4 +10,4 @@ PUBLIC_SERVER_PORT=3000
# API
# 后端 API 基础地址
PUBLIC_API_BASE_URL=http://127.0.0.1:8084/api
PUBLIC_API_BASE_URL=http://127.0.0.1:8085/api
-1
View File
@@ -1,5 +1,4 @@
{
"editor.cursorSmoothCaretAnimation": "on",
"oxc.fmt.configPath": ".oxfmtrc.json",
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true,
+9
View File
@@ -22,11 +22,17 @@ declare module 'vue' {
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSplitter: typeof import('element-plus/es')['ElSplitter']
ElSplitterPanel: typeof import('element-plus/es')['ElSplitterPanel']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElText: typeof import('element-plus/es')['ElText']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree']
@@ -42,4 +48,7 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}
+3 -3
View File
@@ -17,8 +17,6 @@
"nprogress": "^0.2.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^31.0.0",
"uuid": "^13.0.0",
"vue": "^3.5.26",
"vue-router": "^5.0.3",
@@ -37,6 +35,8 @@
"@rsbuild/plugin-vue-jsx": "^1.1.2",
"oxfmt": "^0.40.0",
"resize-observer-polyfill": "^1.5.1",
"unplugin-icons": "^23.0.1"
"unplugin-icons": "^23.0.1",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^31.0.0"
}
}
+7 -2
View File
@@ -1,7 +1,12 @@
<template>
<router-view />
<el-config-provider :locale="zhCn">
<router-view />
</el-config-provider>
</template>
<script lang="js" setup></script>
<script lang="js" setup>
import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/es/locale/lang/zh-cn";
</script>
<style scoped></style>
@@ -183,8 +183,6 @@ function _fetchOptions(searchTitle) {
: condition;
}
if (!inputChange.value) searchCondition = "";
loadPageDataApi({
page: pagination.page,
size: pagination.size,
@@ -44,6 +44,10 @@ const menuList = ref([
label: "字段配置",
path: "/system/field",
},
{
label: "权限配置",
path: "/system/permission",
},
],
},
]);
@@ -151,6 +155,7 @@ function handleMenuChange(val) {
}
.el-menu-item {
padding-left: 48px !important;
margin-bottom: 4px;
}
&.is-active {
.el-sub-menu__title {
@@ -1,4 +1,9 @@
<script lang="js" setup>
import { useUserStore } from "@/store/user";
import { useTabStore } from "@/store/tab";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
const props = defineProps({
collapse: {
type: Boolean,
@@ -8,9 +13,23 @@ const props = defineProps({
const emits = defineEmits(["collapse"]);
const router = useRouter();
const tabStore = useTabStore();
const userStore = useUserStore();
function onCollapse() {
emits("collapse");
}
function logout() {
userStore.resetUserInfo();
tabStore.resetTabs();
ElMessage.success("已成功退出");
router.push("/login");
}
</script>
<template>
@@ -18,24 +37,29 @@ function onCollapse() {
<div class="g3-topbar-left">
<div class="g3-action-icon" @click="onCollapse()">
<G3Icon
:icon="
props.collapse
? 'lucide:list-indent-increase'
: 'lucide:list-indent-decrease'
"
:icon="props.collapse ? 'lucide:list-indent-increase' : 'lucide:list-indent-decrease'"
/>
</div>
</div>
<div class="g3-topbar-right">
<div class="user-card compact">
<G3Icon icon="lucide:circle-user-round" size="18" />
<div class="user-info">
<span class="user-name">001</span>
<!-- <span class="divider">·</span>
<span class="user-org">g3cd</span> -->
<el-dropdown>
<div class="user-card compact">
<G3Icon icon="lucide:circle-user-round" size="18" />
<div class="user-info">
<span class="user-name">
{{ userStore.userInfo.username || userStore.userInfo.userid || "空" }}
</span>
</div>
</div>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item> 个人信息 </el-dropdown-item>
<el-dropdown-item> 修改密码 </el-dropdown-item>
<el-dropdown-item divided @click="logout"> 退出登录 </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
+3
View File
@@ -33,6 +33,9 @@ VxeUITable.setConfig({
footerCellConfig: {
height: 32,
},
columnDragConfig: {
showIcon: false,
},
},
});
@@ -30,6 +30,14 @@ export const routes = [
title: "字段配置",
keepAlive: true,
},
},
{
path: "/system/permission",
component: () => import("@/views/system/permission/index.vue"),
meta: {
title: "权限管理",
keepAlive: true,
},
},
],
},
@@ -20,6 +20,7 @@ export const useUserStore = defineStore(
function resetUserInfo() {
userInfo.value.token = "";
userInfo.value.username = "";
}
function isLogined() {
@@ -19,7 +19,3 @@
.el-button--large {
height: 32px;
}
.el-button + .el-button {
margin-left: 0px ;
}
@@ -1,23 +1,26 @@
.vxe-table {
.el-checkbox {
height: 100%;
}
.row--current {
.col--edit {
.vxe-cell {
padding: 1px !important;
.col--edit {
.vxe-cell {
padding: 1px !important;
.vxe-cell--wrapper {
height: 100%;
.el-input,
.el-select {
.vxe-cell--wrapper {
height: 100%;
line-height: 100%;
.el-input__wrapper,
.el-select__wrapper {
.el-input,
.el-select {
height: 100%;
line-height: 100%;
.el-input__wrapper,
.el-select__wrapper {
height: 100%;
border-radius: 0;
}
}
.el-checkbox {
height: 100%;
border-radius: 0;
}
}
}
@@ -694,12 +694,12 @@ function handleCheckColumns() {
.content-head-left {
display: flex;
@include flexGapWrap(4px, 8px);
@include flexGapWrap(4px, 0px);
}
.content-head-right {
display: flex;
@include flexGapWrap(4px, 8px);
@include flexGapWrap(4px, 0px);
flex-wrap: nowrap;
min-width: 200px;
}
@@ -0,0 +1,700 @@
<script lang="js" setup>
import { loadPageDataApi, saveObjectApi, loadDataApi } from "@/request/api";
import { useUserStore } from "@/store/user";
import { diffObject } from "@/utils/diff";
import PermissionModel from "./permission-model.vue";
import PermissionCopyModal from "./permission-copy-modal.vue";
const userStore = useUserStore();
const loading = reactive({
table: false,
save: false,
});
const tableRef = ref();
const tableData = ref([]);
let oldRowData = {};
const selectCurrentRow = ref(null);
const isSelectedRow = computed(() => selectCurrentRow.value !== null);
const selectedIds = ref([]);
const pagination = reactive({
page: 1,
size: 20,
total: 0,
});
const searchForm = ref({
name: "",
});
const showPasswordRow = ref(false);
// 授予权限
const dialogVisible = ref(false);
const dialogData = ref({});
// 复制权限
const copyDialogVisible = ref(false);
onMounted(() => {
getTableData();
});
function getTableData() {
loading.table = true;
const userid = userStore.userInfo.userid;
let searchCondition = "";
if (searchForm.value.name !== "") {
searchCondition = ` (b_id LIKE '%${searchForm.value.name}%' OR b_name LIKE '%${searchForm.value.name}%')`;
}
if (userid !== "g3soft") {
searchCondition += `AND b_id != 'g3soft' `;
}
loadPageDataApi({
page: pagination.page,
size: pagination.size,
viewName: "b_user",
searchCondition,
orderField: "b_id asc",
})
.then((res) => {
if (res.success) {
tableData.value = res.data.rows;
pagination.total = res.data.total;
}
})
.finally(() => {
loading.table = false;
});
}
// 分页变化
function handleSizeChange(val) {
pagination.page = val;
getTableData();
}
function handleCurrentChange(val) {
pagination.size = val;
getTableData();
}
// 进入单行编辑
async function startEditRow(row) {
const $table = tableRef.value;
if ($table) {
oldRowData = JSON.parse(JSON.stringify(row));
await $table.setEditRow(row);
await $table.setCurrentRow(row);
selectCurrentRow.value = row;
}
}
// 取消单行编辑
async function cancelEditRow(row) {
const $table = tableRef.value;
if ($table) {
await $table.clearEdit();
// 还原数据
await $table.revertData(row);
selectCurrentRow.value = null;
}
}
// 判断是否在编辑态
function isRowEditing(row) {
const $table = tableRef.value;
if ($table) {
return $table.isEditByRow(row);
}
}
// 编辑关闭事件
async function handleEditClosed({ row, rowIndex, column, columnIndex }) {
const $table = tableRef.value;
if ($table) {
await $table.clearEdit();
// 还原数据
await $table.revertData(row);
}
}
// 编辑激活事件
function handleEditActivated({ row }) {
startEditRow(row);
}
// 保存本地提交
async function commitEditRow(row) {
const $table = tableRef.value;
if ($table) {
// 重新加载行
await $table.reloadRow(row, {});
await $table.clearEdit();
}
}
async function handleAddRow() {
const $table = tableRef.value;
if ($table) {
if ($table.getInsertRecords().length > 0) {
$table.setEditRow($table.getInsertRecords()[0], true);
return;
}
const record = {
b_canuse: "1",
b_paylevel: "5",
b_receivelevel: "5",
b_logintime: "1",
b_id: "",
b_name: "",
b_department: "",
b_salesman: "0",
b_dealman: "0",
b_docman: "0",
b_password: "",
b_subcode: "",
b_bz: "",
b_levelvisible: "",
};
const { row: newRow } = await $table.insertAt(record, -1);
$table.setEditRow(newRow, true);
$table.setCurrentRow(newRow);
}
}
// 删除单行
async function handleDeleteRow(row) {
if (!row) return;
const $table = tableRef.value;
if ($table) {
deleteRows([row]);
}
}
// 删除多选行
async function handleDeleteRows() {
const $table = tableRef.value;
if ($table) {
deleteRows($table.getCheckboxRecords());
}
}
// 删除(传入行数组)
async function deleteRows(rows = []) {
const $table = tableRef.value;
if (!$table) {
return;
}
if (!rows.length) {
ElMessage.warning("请先选择要删除的行");
return;
}
const selectedIds = rows.filter((row) => row.b_id !== "g3soft").map((row) => row.b_id);
const delectedIds = rows
.filter((row) => !$table.isInsertByRow(row) && row.b_id !== "g3soft")
.map((row) => row.b_id);
const tipMessage = `确定要删除以下账号吗?<br/><strong>${selectedIds.join(", ")}</strong>`;
await ElMessageBox.confirm(tipMessage, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
dangerouslyUseHTMLString: true,
});
if (delectedIds.length === 0) {
ElMessage.success("删除成功");
getTableData();
return;
}
const requestData = [
{
tableName: "b_user",
keyField: "b_id",
inserts: [],
updates: [],
deletes: delectedIds.map((id) => ({ b_id: id })),
},
{
tableName: "b_user_qx",
keyField: "father",
deletes: delectedIds.map((id) => ({ father: id })),
inserts: [],
updates: [],
},
{
tableName: "b_user_power",
keyField: "userid",
deletes: delectedIds.map((id) => ({ userid: id })),
inserts: [],
updates: [],
},
];
await saveObjectApi(requestData).then((res) => {
if (res.success) {
ElMessage.success("删除成功");
getTableData();
}
});
}
// 保存行(新增或更新)
async function handleSaveRow(row) {
const $table = tableRef.value;
const isNew = $table.isInsertByRow(row);
const editId = row.b_id.trim().toUpperCase() || "";
if (!editId) {
ElMessage.warning("账号不能为空");
return;
}
// 1. 检查表格内重复(本地数据)
const duplicate = tableData.value.some(
(item) => item.b_id?.trim().toUpperCase() === editId && item.b_id !== row.b_id, // 排除自己
);
if (duplicate) {
ElMessage.warning("账号重复,请修改");
return;
}
loading.save = true;
try {
// 2. 检查后端重复
let whereClause = `b_id = '${editId}'`;
if (!isNew) {
// 更新时排除当前记录
whereClause += ` AND b_id <> '${editId || ""}'`;
}
const idResponse = await loadDataApi("b_user", whereClause);
if (idResponse.success && idResponse.data.length > 0) {
ElMessage.warning("编号重复,请修改");
return;
}
// 3. 组装请求
let requestData = [];
if (isNew) {
// 新增
const editedData = { ...toRaw(row) };
delete editedData._X_ROW_KEY;
requestData = [
{
tableName: "b_user",
keyField: "b_id",
inserts: [editedData],
updates: [],
deletes: [],
},
];
} else {
// 更新
const diffResult = diffObject(row, oldRowData, ["b_id"], []);
if (!diffResult.isChanged) {
ElMessage.info("没有数据变更");
return;
}
requestData = [
{
tableName: "b_user",
keyField: "b_id",
inserts: [],
updates: [diffResult.result],
deletes: [],
},
];
}
const saveRes = await saveObjectApi(requestData);
if (saveRes.success) {
ElMessage.success("保存成功");
const $table = tableRef.value;
if ($table) {
if (isNew) {
const currentData = tableData.value;
currentData.push(row);
commitEditRow(row);
} else {
commitEditRow(row);
}
}
}
} finally {
loading.save = false;
}
}
function handleShowDialog(row) {
dialogVisible.value = true;
dialogData.value = row;
}
function handleShowCopyDialog() {
const $table = tableRef.value;
if ($table) {
if (!selectCurrentRow.value) {
ElMessage.warning("请选中需要复制的行");
return;
}
if ($table.isInsertByRow(selectCurrentRow.value)) {
ElMessage.warning("请保存后再复制权限");
return;
}
selectedIds.value = [selectCurrentRow.value.b_id];
copyDialogVisible.value = true;
}
}
function handleShowBatchCopyDialog() {
const $table = tableRef.value;
if ($table) {
const records = $table.getCheckboxRecords();
const filteredRecords = records.filter((item) => !$table.isInsertByRow(item));
const bIds = filteredRecords.map((item) => item.b_id);
if (bIds.length === 0) {
ElMessage.warning("请选择需要复制的用户");
return;
}
selectedIds.value = bIds;
copyDialogVisible.value = true;
}
}
</script>
<template>
<div class="g3-container">
<div class="header">
<div class="header-left">
<el-button type="primary" @click="handleAddRow()">
<el-icon class="el-icon--left">
<G3Icon icon="lucide:plus" />
</el-icon>
新建
</el-button>
<el-button type="danger" @click="handleDeleteRows()">
<el-icon class="el-icon--left">
<G3Icon icon="lucide:trash-2" />
</el-icon>
批量删除
</el-button>
<el-button type="primary" @click="showPasswordRow = !showPasswordRow">查看密码</el-button>
<el-button type="primary" @click="handleShowBatchCopyDialog">批量复制权限</el-button>
<el-button type="primary" @click="handleShowCopyDialog" :disabled="!isSelectedRow">
复制权限
</el-button>
<el-button type="primary" :disabled="!isSelectedRow">员工资料</el-button>
</div>
<div class="header-right">
<el-input placeholder="搜索账号、用户名" v-model="searchForm.name" />
<el-button type="primary" @click="getTableData()">
<el-icon class="el-icon--left">
<G3Icon icon="lucide:search" />
</el-icon>
搜索
</el-button>
</div>
</div>
<div class="content">
<vxe-table
ref="tableRef"
border
stripe
header-align="center"
keep-source
:loading="loading.table"
:data="tableData"
:sort-config="{
trigger: 'cell',
}"
:row-config="{
useKey: true,
// keyField: 'b_id',
isCurrent: true,
isHover: true,
}"
:column-config="{
resizable: true,
drag: true,
}"
:edit-config="{
trigger: 'click',
mode: 'row',
autoClear: false,
showStatus: true,
showIcon: false,
}"
height="100%"
show-overflow
show-header-overflow
@edit-closed="handleEditClosed"
@edit-activated="handleEditActivated"
>
<vxe-column type="checkbox" width="50" align="center" />
<vxe-column
field="b_id"
title="账号"
align="center"
sortable
width="100"
:edit-render="{
autoFocus: true,
}"
>
<template #edit="{ row }">
<el-input v-model="row.b_id" size="small" />
</template>
</vxe-column>
<vxe-column
field="b_name"
title="用户名"
align="center"
sortable
width="100"
:edit-render="{
autoFocus: true,
}"
>
<template #edit="{ row }">
<el-input v-model="row.b_name" size="small" />
</template>
</vxe-column>
<vxe-column
field="b_department"
title="部门"
align="center"
sortable
width="100"
:edit-render="{
autoFocus: true,
}"
>
<template #edit="{ row }">
<el-input v-model="row.b_department" size="small" />
</template>
</vxe-column>
<vxe-column
field="b_subcode"
title="公司"
align="center"
sortable
width="100"
:edit-render="{
autoFocus: true,
}"
>
<template #edit="{ row }">
<el-input v-model="row.b_subcode" size="small" />
</template>
</vxe-column>
<vxe-column
field="b_canuse"
title="启用"
align="center"
sortable
width="100"
:edit-render="{
autoFocus: true,
}"
>
<template #default="{ row }">
<el-checkbox v-model="row.b_canuse" true-value="1" false-value="0" />
</template>
<template #edit="{ row }">
<el-checkbox v-model="row.b_canuse" true-value="1" false-value="0" />
</template>
</vxe-column>
<vxe-column
v-if="showPasswordRow"
field="b_password"
title="密码"
align="center"
sortable
width="100"
:edit-render="{
autoFocus: true,
}"
>
<template #edit="{ row }">
<el-input v-model="row.b_password" size="small" />
</template>
</vxe-column>
<vxe-column
field="b_bz"
title="备注"
align="center"
sortable
width="100"
:edit-render="{
autoFocus: true,
}"
>
<template #edit="{ row }">
<el-input v-model="row.b_bz" size="small" />
</template>
</vxe-column>
<vxe-column
v-if="showPasswordRow"
field="b_password"
title="密码"
align="center"
sortable
width="100"
:edit-render="{
autoFocus: true,
}"
>
<template #edit="{ row }">
<el-input v-model="row.b_password" size="small" />
</template>
</vxe-column>
<vxe-column title="操作" width="180" align="center">
<template #default="{ row }">
<template v-if="isRowEditing(row)">
<el-button
type="success"
link
:loading="loading.save"
@click="handleSaveRow(row)"
@click.stop
>
保存
</el-button>
<el-button link @click="cancelEditRow(row)" @click.stop style="margin-left: 6px">
取消
</el-button>
</template>
<template v-else>
<el-button
:disabled="loading.save"
type="primary"
link
@click.stop
@click="startEditRow(row)"
>
编辑
</el-button>
<el-button
:disabled="loading.save"
type="primary"
link
@click="handleShowDialog(row)"
@click.stop
style="margin-left: 6px"
>
分配权限
</el-button>
<el-button
style="margin-left: 6px"
:disabled="loading.save"
type="danger"
link
@click="handleDeleteRow(row)"
>
删除
</el-button>
</template>
</template>
</vxe-column>
</vxe-table>
</div>
<div class="footer">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.size"
:total="pagination.total"
:page-sizes="[20, 50, 100, 500]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<PermissionModel v-model="dialogVisible" :data="dialogData" />
<PermissionCopyModal v-model="copyDialogVisible" :ids="selectedIds" />
</div>
</template>
<style lang="scss" scoped>
@use "@/styles/mixins.scss" as *;
.g3-container {
background-color: #fff;
border-radius: 4px;
height: 100%;
padding: 8px;
display: flex;
flex-direction: column;
@include flexGapColumn(8px);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
.header-left {
display: flex;
@include flexGapWrap(4px, 0px);
}
.header-right {
display: flex;
@include flexGapWrap(4px, 8px);
flex-wrap: nowrap;
min-width: 200px;
}
}
.content {
flex: 1;
}
.footer {
display: flex;
align-items: flex-end;
justify-content: flex-end;
}
</style>
@@ -0,0 +1,179 @@
<script setup>
import { loadDataApi, saveObjectKeysApi } from "@/request/api";
import { ElMessage, ElMessageBox } from "element-plus";
import { reactive, ref, watch } from "vue";
const props = defineProps({
modelValue: Boolean,
ids: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["update:modelValue"]);
const formData = ref({
b_id: "",
b_name: "",
});
const visible = ref(props.modelValue);
watch(
() => props.modelValue,
(v) => (visible.value = v),
);
watch(visible, (v) => emit("update:modelValue", v));
const loading = reactive({
save: false,
});
function close() {
formData.value.b_id = "";
formData.value.b_name = "";
visible.value = false;
}
function handleClosed() {
close();
}
async function handleSave() {
if (formData.value.b_id === "") {
ElMessage.warning("请选择需要复制的权限");
return;
}
try {
// 确保目标用户的 ids 数组不为空
if (props.ids && props.ids.length === 0) {
ElMessage.warning("请选择目标用户");
return;
}
await ElMessageBox.confirm("原帐号权限将被清空,是否复制权限?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
dangerouslyUseHTMLString: true,
});
loading.save = true;
const fromUserId = formData.value.b_id;
const toUserIds = props.ids; // 目标用户的 IDs 数组
// 1. 删除目标用户的原有权限(循环处理所有目标用户)
const deleteRequests = toUserIds
.map((toUserId) => ({
tableName: "b_user_qx",
keyFields: ["father"],
deletes: [{ father: toUserId }],
inserts: [],
updates: [],
}))
.concat(
toUserIds.map((toUserId) => ({
tableName: "b_user_power",
keyFields: ["userid"],
deletes: [{ userid: toUserId }],
inserts: [],
updates: [],
})),
);
const res1 = await saveObjectKeysApi(deleteRequests);
if (!res1.success) {
return;
}
// 2. 查询来源用户的权限数据
const [qxRes, powerRes] = await Promise.all([
loadDataApi("b_user_qx", `father='${fromUserId}'`),
loadDataApi("b_user_power", `userid='${fromUserId}'`),
]);
// 3. 构造批量插入数据(循环处理目标用户)
const insertRequests = toUserIds
.map((toUserId) => [
{
tableName: "b_user_qx",
keyFields: ["father", "ModuleID"],
inserts: (qxRes.data || []).map((r) => {
const { b_id, ...rest } = r;
return { ...rest, father: toUserId };
}),
updates: [],
deletes: [],
},
{
tableName: "b_user_power",
keyFields: ["UserID", "ModuleID", "b_ivalue"],
inserts: (powerRes.data || []).map((r) => {
const { b_id, ...rest } = r;
return { ...rest, UserID: toUserId };
}),
updates: [],
deletes: [],
},
])
.flat();
const res2 = await saveObjectKeysApi(insertRequests);
if (!res2.success) {
return;
}
close();
ElMessage.success("复制成功");
} catch (err) {
console.error(err);
ElMessage.error("复制失败");
} finally {
loading.save = false;
}
}
</script>
<template>
<el-dialog
v-bind="$attrs"
v-model="visible"
title="权限复制"
width="400px"
class="dialog"
@closed="handleClosed"
>
<div class="dialog-content">
<span>请选择复制账号</span>
<G3Select
v-model="formData.b_id"
v-model:label="formData.b_name"
search-condition="b_id <> 'g3soft'"
view-name="b_user"
value-key="b_id"
label-key="b_name"
style="margin-top: 4px"
/>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="close"> 取消 </el-button>
<el-button type="primary" :loading="loading.save" @click="handleSave"> 确认 </el-button>
</div>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.dialog-content {
display: flex;
flex-direction: column;
overflow-y: auto;
}
</style>
@@ -0,0 +1,679 @@
<script setup>
import { ElMessage, ElMessageBox } from "element-plus";
import PermissionUserModel from "./permission-user-model.vue";
import { useUserStore } from "@/store/user";
import { loadDataBySqlApi, loadDataApi, saveObjectKeysApi } from "@/request/api";
const props = defineProps({
data: {
type: null,
required: true,
},
});
const userStore = useUserStore();
const treeRef = ref();
const userQx = reactive({
moduleId: "",
has: false,
QxType: "", // userModuleQx
ShareType: "", // userShareQx
});
const userVisible = ref(false);
const tableData = ref([]);
const loading = reactive({
table: false,
actions: false,
});
const defaultProps = {
children: "children",
label: "mc_b",
};
async function handleNodeClick(nodeData) {
loadNodeData(nodeData);
}
async function loadNode(node, resolve) {
try {
const { level, data: nodeData } = node;
let sql = "";
// 根节点查询
if (level === 0) {
sql = `SELECT * FROM s_module_bmfl WHERE js='1' AND b_canuse='1' ORDER BY b_xh`;
} else {
if (!nodeData) {
resolve([]);
return;
}
const parentField = `bm_${level}_b`;
const parentCondition = ` AND ${parentField}='${nodeData.b_id}'`;
const childLevel = level + 1;
sql = `
SELECT s_module_bmfl.*, s_module.b_id AS b_moduleid
FROM s_module_bmfl
LEFT JOIN s_module ON s_module.b_cate_id = s_module_bmfl.b_id
WHERE s_module_bmfl.b_canuse='1'
AND js='${childLevel}'
${parentCondition}
AND ISNULL(s_module.b_ISVirtualModule,'')<>'1'
ORDER BY b_xh
`;
}
const res = await loadDataBySqlApi(sql);
if (res.success) {
const records = res.data || [];
if (records.length === 0) {
loadNodeData(nodeData);
}
resolve(records);
} else {
resolve([]);
}
} catch (err) {
console.error("loadNode 异常", err);
resolve([]);
}
}
async function loadNodeData(nodeData) {
if (!nodeData) return;
// 防止重复加载
// if (loading.table) {
// ElMessage.warning('请不要点击过于频繁!')
// return
// }
loading.table = true;
userQx.moduleId = nodeData.b_moduleid;
try {
// 并行请求
const [userQxReq, userPowerReq] = await Promise.all([
loadDataApi(
"b_user_qx",
`father = '${props.data.b_id}' and ModuleID = '${nodeData.b_moduleid}'`,
),
loadDataApi(
"b_user_power",
`userid = '${props.data.b_id}' and moduleid='${nodeData.b_moduleid}'`,
),
]);
// 处理 userQx
if (userQxReq.success && userQxReq.data.length > 0) {
const userQxData = userQxReq.data[0];
userQx.QxType = userQxData.QxType;
userQx.ShareType = userQxData.ShareType;
userQx.has = true;
} else {
userQx.QxType = "";
userQx.ShareType = "";
userQx.has = false;
}
// 处理 userPower
if (userPowerReq.success) {
tableData.value = userPowerReq.data || [];
}
} catch (err) {
console.error(err);
} finally {
loading.table = false;
}
}
async function updateShareTypeApi(value, userId, moduleId, hasQx) {
const requestData = [
{
tableName: "b_user_qx",
keyFields: ["father", "ModuleID"],
inserts: [],
updates: [],
deletes: [],
},
];
const payload = {
father: userId,
ModuleID: moduleId,
ShareType: value,
};
if (hasQx) {
requestData[0].updates.push(payload);
} else {
requestData[0].inserts.push(payload);
}
const response = await axios.post("/saveobjectV3", requestData, {
headers: { "Content-Type": "application/json" },
});
if (!response.data?.success) throw new Error(response.data?.message || "保存失败");
userStore.initPermission();
return payload;
}
async function handleUpdateShareType(value) {
if (!userQx.moduleId) {
ElMessage.warning("请选择模块");
return;
}
loading.actions = true;
try {
const payload = await updateShareTypeApi(value, props.data.b_id, userQx.moduleId, userQx.has);
userQx.ShareType = payload.ShareType;
userQx.has = true;
ElMessage.success("保存成功");
userStore.initPermission();
} catch (err) {
console.error(err);
ElMessage.error(err.message || "保存失败");
} finally {
loading.actions = false;
}
}
async function updateQxTypeApi(value, userId, moduleId, hasQx) {
const requestData = [
{
tableName: "b_user_qx",
keyFields: ["father", "ModuleID"],
inserts: [],
updates: [],
deletes: [],
},
];
if (hasQx) {
// 更新
requestData[0].updates.push({
father: userId,
ModuleID: moduleId,
QxType: value,
});
} else {
// 插入
requestData[0].inserts.push({
father: userId,
ModuleID: moduleId,
QxType: value,
});
}
const response = await saveObjectKeysApi(requestData);
if (!response.success) {
userStore.initPermission();
throw new Error(response.data?.message || "保存失败");
}
return response.data;
}
async function handleUpdateQxType(value) {
if (!userQx.moduleId) {
ElMessage.warning("请选择模块");
return;
}
loading.actions = true;
try {
await updateQxTypeApi(value, props.data.b_id, userQx.moduleId, userQx.has);
ElMessage.success("保存成功");
userQx.QxType = value;
userQx.has = true;
// userStore.initPermission();
} catch (err) {
console.error(err);
} finally {
loading.actions = false;
}
}
// 单条更新
async function updateQxCanuseApi(value, userId, row) {
const queryRes = await loadDataApi(
"b_user_power",
`UserID = '${userId}' and ModuleID = '${row.ModuleID}' and b_ivalue = '${row.b_ivalue}'`,
);
if (!queryRes.success) {
return;
}
const records = queryRes.data || [];
const isUpdate = records.length > 0;
const requestData = [
{
tableName: "b_user_power",
keyFields: ["UserID", "ModuleID", "b_ivalue"],
inserts: [],
updates: [],
deletes: [],
},
];
const payload = {
b_canuse: value,
UserID: userId,
ModuleID: row.ModuleID,
b_ivalue: row.b_ivalue,
b_xh: row.b_xh,
b_caption: row.b_caption,
b_des: row.b_des,
};
if (isUpdate) requestData[0].updates.push(payload);
else requestData[0].inserts.push(payload);
const saveRes = await saveObjectKeysApi(requestData);
if (!saveRes.success) throw new Error(saveRes.message || "保存失败");
return payload; // 返回本次更新的数据
}
// 单条更新
async function handleUpdateQxCanuse(value, row) {
try {
const payload = await updateQxCanuseApi(value, props.data.b_id, row);
row.b_canuse = payload.b_canuse;
userQx.has = true;
ElMessage.success("保存成功");
// userStore.initPermission();
return true;
} catch (err) {
console.error(err);
return false;
}
}
// 批量更新
async function handleUpdateCheckAllCanuse(value) {
loading.actions = true;
try {
const strValue = value ? "1" : "0";
// 找出 b_canuse 与 strValue 不一致的行
const mismatchedRows = tableData.value.filter((item) => item.b_canuse !== strValue);
if (mismatchedRows.length === 0) {
ElMessage.warning("暂无更新");
return;
}
// 构建更新数据
const updates = mismatchedRows.map((item) => ({
UserID: item.UserID,
ModuleID: item.ModuleID,
b_ivalue: item.b_ivalue,
b_canuse: strValue,
}));
const requestData = [
{
tableName: "b_user_power",
keyFields: ["UserID", "ModuleID", "b_ivalue"],
inserts: [],
updates,
deletes: [],
},
];
// 发起请求
const response = await saveObjectKeysApi(requestData);
if (response.data?.success) {
// 本地更新 tableData
mismatchedRows.forEach((item) => (item.b_canuse = strValue));
ElMessage.success("批量更新成功");
// userStore.initPermission();
} else {
throw new Error(response.data?.message || "更新失败");
}
} catch (err) {
console.error(err);
} finally {
loading.actions = false;
}
}
async function handleUpdateAllQx(qxtype) {
try {
loading.actions = true;
const userid = props.data.b_id;
const canuse = qxtype === "完全" ? "1" : "0";
const requestData = [
{
tableName: "b_user_qx",
keyFields: ["father", "ModuleID"],
inserts: [],
updates: [],
deletes: [],
},
{
tableName: "b_user_power",
keyFields: ["UserID", "ModuleID", "b_ivalue"],
inserts: [],
updates: [],
deletes: [],
},
];
// 查询 b_user_qx 和 b_user_power
const [qxRes, powerRes] = await Promise.all([
loadDataBySqlApi(`SELECT * FROM b_user_qx WHERE father='${userid}'`),
loadDataBySqlApi(`SELECT * FROM b_user_power WHERE UserID='${userid}'`),
]);
const existingQx = qxRes.success ? qxRes.data || [] : [];
const existingPower = powerRes.success ? powerRes.data || [] : [];
// 处理 b_user_qx
const moduleSql = `SELECT b_id AS ModuleID FROM s_module WHERE isnull(b_ISVirtualModule,'')<>''`;
const moduleRes = await loadDataBySqlApi(moduleSql);
const moduleIds = moduleRes.data.map((r) => r.ModuleID) || [];
moduleIds.forEach((mid) => {
const exist = existingQx.find((r) => r.ModuleID === mid);
if (exist) {
requestData[0].updates.push({
father: userid,
ModuleID: mid,
QxType: qxtype,
ShareType: exist.ShareType || "所有可见",
ControlType: exist.ControlType || "所有控制",
});
} else {
requestData[0].inserts.push({
father: userid,
ModuleID: mid,
QxType: qxtype,
ShareType: "所有可见",
ControlType: "所有控制",
});
}
});
// 处理 b_user_power
const powerListRes = await loadDataBySqlApi(
`SELECT ModuleID, b_ivalue, b_xh, b_caption,b_des FROM s_module_power`,
);
const modulePowerList = powerListRes.data || [];
modulePowerList.forEach((mp) => {
const exist = existingPower.find(
(p) => p.ModuleID === mp.ModuleID && p.b_ivalue === mp.b_ivalue,
);
if (exist) {
requestData[1].updates.push({
UserID: userid,
ModuleID: mp.ModuleID,
b_ivalue: mp.b_ivalue,
b_canuse: canuse,
});
} else {
requestData[1].inserts.push({
UserID: userid,
ModuleID: mp.ModuleID,
b_ivalue: mp.b_ivalue,
b_xh: mp.b_xh,
b_caption: mp.b_caption,
b_canuse: canuse,
});
}
});
// 调用增删改接口
const saveRes = await saveObjectKeysApi(requestData);
if (!saveRes?.success) throw new Error(saveRes.message || "保存失败");
// 更新本地 tableData
tableData.value.forEach((item) => (item.b_canuse = canuse));
ElMessage.success("操作成功");
// userStore.initPermission();
} catch (err) {
console.error(err);
} finally {
loading.actions = false;
}
}
async function handleDeleteAllQx() {
try {
await ElMessageBox.confirm("确定要将这个帐号的权限清空吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "error",
dangerouslyUseHTMLString: true,
});
loading.actions = true;
const userid = props.data.b_id;
const requestData = [
{
tableName: "b_user_qx",
keyFields: ["father"],
inserts: [],
updates: [],
deletes: [{ father: userid }],
},
{
tableName: "b_user_power",
keyFields: ["userid"],
inserts: [],
updates: [],
deletes: [{ userid }],
},
];
const saveRes = await saveObjectKeysApi(requestData);
if (saveRes.success) {
ElMessage.success("清理成功");
tableData.value = [];
// userStore.initPermission();
} else {
throw new Error(saveRes.data?.message || "清理失败");
}
} catch (err) {
console.error(err);
} finally {
loading.actions = false;
}
}
function handleClosed() {
userQx.moduleId = "";
userQx.has = false;
userQx.QxType = "";
userQx.moduleId = "";
tableData.value = [];
loading.table = false;
loading.actions = false;
}
</script>
<template>
<el-dialog v-bind="$attrs" title="权限设置" width="1000px" class="dialog" @closed="handleClosed">
<div class="dialog-header">
<div>
<div>账号:{{ props.data.b_id }}</div>
<div>用户名:{{ props.data.b_name }}</div>
<div>部门:{{ props.data.b_department }}</div>
</div>
<div>
<el-button
size="small"
type="primary"
:loading="loading.actions"
@click="handleUpdateAllQx('完全')"
>
全授权
</el-button>
<el-button
size="small"
type="danger"
:loading="loading.actions"
@click="handleUpdateAllQx('0')"
>
全禁用
</el-button>
<el-button size="small" type="danger" :loading="loading.actions" @click="handleDeleteAllQx">
清理权限
</el-button>
</div>
</div>
<div class="dialog-content">
<div class="dialog-left">
<div class="left-header">功能模块</div>
<el-tree
ref="treeRef"
class="tree"
:props="defaultProps"
:load="loadNode"
lazy
@node-click="handleNodeClick"
/>
</div>
<div class="dialog-right">
<div v-if="userQx.moduleId !== ''" class="actions">
<div class="actions-item">
<el-checkbox
v-model="userQx.QxType"
label="使用权限"
true-value="完全"
false-value="禁用"
:disabled="loading.actions"
@change="handleUpdateQxType"
/>
<el-checkbox label="全选" @change="handleUpdateCheckAllCanuse" />
</div>
<el-radio-group
v-model="userQx.ShareType"
:disabled="loading.actions"
@change="handleUpdateShareType"
>
<el-radio value="所有可见"> 所有可见 </el-radio>
<el-radio value="公司可见"> 公司可见 </el-radio>
<el-radio value="部门可见"> 部门可见 </el-radio>
<el-radio value="仅本人"> 仅本人 </el-radio>
</el-radio-group>
<el-button type="primary" link @click="userVisible = !userVisible"> 同账户 </el-button>
</div>
<el-table v-loading="loading.table" :data="tableData" style="width: 100%" stripe>
<el-table-column type="index" width="50" align="center" fixed />
<el-table-column prop="b_canuse" label="操作授权" width="80" align="center">
<template #default="{ row }">
<el-checkbox
v-model="row.b_canuse"
true-value="1"
false-value="0"
:disabled="loading.actions"
@change="(value) => handleUpdateQxCanuse(value, row)"
/>
</template>
</el-table-column>
<el-table-column prop="b_caption" label="功能说明" width="200" align="center" />
<el-table-column prop="b_des" label="功能描述" align="center" />
</el-table>
</div>
</div>
</el-dialog>
<PermissionUserModel v-model="userVisible" :qx-table-list="tableData" :user-qx-data="userQx" />
</template>
<style lang="scss" scoped>
.tree {
overflow: auto;
}
.dialog-content {
display: flex;
gap: 16px;
height: 500px;
}
.dialog-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.dialog-header div {
display: flex;
gap: 8px;
}
.dialog-left {
width: 30%;
display: flex;
flex-direction: column;
border-right: 1px solid #e0e0e0;
}
.left-header {
font-weight: bold;
margin-bottom: 12px;
}
.module-tree {
flex: 1;
overflow-y: auto;
}
.dialog-right {
flex: 1;
display: flex;
flex-direction: column;
}
.dialog-right .actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.dialog-right .actions .actions-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.el-radio {
margin-right: 16px;
}
</style>
@@ -0,0 +1,235 @@
<script setup lang="js">
import { ElMessage } from "element-plus";
import { useUserStore } from "@/store/user";
import { loadDataApi, saveObjectKeysApi } from "@/request/api";
const userStore = useUserStore();
const props = defineProps({
modelValue: Boolean,
userQxData: {
type: Object,
default: () => ({}),
},
qxTableList: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["update:modelValue"]);
const visible = ref(props.modelValue);
watch(
() => props.modelValue,
(v) => (visible.value = v),
);
watch(visible, (v) => emit("update:modelValue", v));
const tableData = ref([]);
const selectedRows = ref([]);
const loading = reactive({
table: true,
save: false,
});
// 选择行变化
const handleSelectionChange = (rows) => {
selectedRows.value = rows;
};
const close = () => {
tableData.value = [];
selectedRows.value = [];
visible.value = false;
};
const handleOpen = async () => {
loading.table = true;
try {
const response = await loadDataApi("b_user", `b_id <> 'g3soft'`);
if (response.success) {
tableData.value = response.data || [];
} else {
ElMessage.error(response.message || "加载节点失败");
}
} catch (err) {
console.error("加载节点异常:", err);
} finally {
loading.table = false;
}
};
const handleClosed = () => {
tableData.value = [];
selectedRows.value = [];
loading.table = true;
loading.save = false;
};
const handleSave = async () => {
if (selectedRows.value.length === 0) {
close();
return;
}
loading.save = true; // 开始保存
try {
const moduleId = props.userQxData.moduleId;
const ivalueList = props.qxTableList.map((item) => item.b_ivalue);
for (const user of selectedRows.value) {
const qxResponse = await loadDataApi(
"b_user_qx",
`father = '${user.b_id}' and ModuleID = '${moduleId}'`,
);
const qxExisting = qxResponse.data || [];
const hasQX = qxExisting.length > 0;
const inserts1 = [];
const updates1 = [];
if (!hasQX) {
inserts1.push({
father: user.b_id,
ModuleID: moduleId,
ShareType: props.userQxData.ShareType,
QxType: props.userQxData.QxType,
});
} else {
const updateRecord = { father: user.b_id, ModuleID: moduleId };
let needUpdate = false;
if (props.userQxData.ShareType !== qxExisting[0].ShareType) {
updateRecord.ShareType = props.userQxData.ShareType;
needUpdate = true;
}
if (props.userQxData.QxType !== qxExisting[0].QxType) {
updateRecord.QxType = props.userQxData.QxType;
needUpdate = true;
}
if (needUpdate) updates1.push(updateRecord);
}
// 查询用户操作权限表 b_user_power
const powerResponse = await loadDataApi(
"b_user_power",
`UserID = '${user.b_id}' and ModuleID = '${moduleId}' and b_ivalue in ('${ivalueList.join("','")}')`,
);
const powerExisting = powerResponse.data || [];
const inserts2 = [];
const updates2 = [];
props.qxTableList.forEach((qxItem) => {
const found = powerExisting.find((r) => r.b_ivalue === qxItem.b_ivalue);
const targetCanuse = qxItem.b_canuse || "0";
if (found) {
if (found.b_canuse !== targetCanuse) {
updates2.push({
UserID: user.b_id,
ModuleID: moduleId,
b_ivalue: qxItem.b_ivalue,
b_canuse: targetCanuse,
});
}
} else {
inserts2.push({
UserID: user.b_id,
ModuleID: moduleId,
b_ivalue: qxItem.b_ivalue,
b_canuse: targetCanuse,
b_xh: qxItem.b_xh,
b_caption: qxItem.b_caption,
b_des: qxItem.b_des,
});
}
});
const requestData = [
{
tableName: "b_user_qx",
keyFields: ["father", "ModuleID"],
inserts: inserts1,
updates: updates1,
deletes: [],
},
{
tableName: "b_user_power",
keyFields: ["UserID", "ModuleID", "b_ivalue"],
inserts: inserts2,
updates: updates2,
deletes: [],
},
];
const response = await saveObjectKeysApi(requestData);
if (!response.success) {
throw new Error(response.message || "保存失败");
}
}
ElMessage.success("保存成功");
close();
// userStore.initPermission();
} catch (err) {
console.error(err);
} finally {
loading.save = false;
}
};
</script>
<template>
<el-dialog
v-bind="$attrs"
v-model="visible"
title="同账号权限分配"
width="600px"
class="dialog"
@open="handleOpen"
@closed="handleClosed"
>
<div class="dialog-content">
<el-table
:data="tableData"
height="400"
stripe
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="b_id" label="账号" width="100" align="center" />
<el-table-column prop="b_name" label="用户名" width="100" align="center" />
<el-table-column prop="b_department" label="部门" align="center" />
</el-table>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="close">取消</el-button>
<el-button type="primary" :loading="loading.save" @click="handleSave">确认</el-button>
</div>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.dialog-content {
display: flex;
gap: 16px;
height: 400px;
overflow-y: auto;
}
</style>
@@ -0,0 +1,125 @@
<script setup>
import { ElMessage } from "element-plus";
import { ref } from "vue";
const loading = ref(false);
const form = ref({
oldPassword: "",
newPassword: "",
confirmPassword: "",
});
function resetFrom() {
form.value.oldPassword = "";
form.value.newPassword = "";
form.value.confirmPassword = "";
}
async function handleConfirm() {
if (form.value.newPassword !== form.value.confirmPassword) {
ElMessage.warning("新密码和确认密码不一致");
return;
}
loading.value = true;
const requestData = {
oldPassword: form.value.oldPassword,
newPassword: form.value.newPassword,
};
try {
const saveRes = await axios.post("/updatePassword", requestData, {
headers: { "Content-Type": "application/json" },
});
if (saveRes.data.success) {
ElMessage.success("密码更新成功");
resetFrom();
} else {
ElMessage.error(`密码更新失败:${saveRes.data.message}`);
}
} catch (error) {
ElMessage.error(`密码更新失败:${error.message || "未知错误"}`);
} finally {
loading.value = false;
}
}
</script>
<template>
<div class="g3-container">
<div class="g3-grid">
<div class="g3-cell">
<div class="g3-cell-label">旧密码:</div>
<el-input v-model="form.oldPassword" type="password" />
</div>
<div class="g3-cell">
<div class="g3-cell-label">新密码:</div>
<div class="g3-cell-value">
<el-input v-model="form.newPassword" type="password" />
</div>
</div>
<div class="g3-cell">
<div class="g3-cell-label">确认密码:</div>
<el-input v-model="form.confirmPassword" type="password" />
</div>
</div>
<div class="g3-actions">
<el-button type="primary" :loading="loading" @click="handleConfirm"> 确认 </el-button>
<el-button :loading="loading" @click="resetFrom"> 重置 </el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.g3-container {
padding: 16px;
width: 400px;
}
.g3-grid {
display: grid;
grid-template-columns: repeat(1, 1fr);
gap: 1px;
border: 1px solid #ddd;
background-color: #ddd;
}
.g3-cell {
display: flex;
align-items: center;
color: #333;
font-size: 14px;
height: 32px;
}
.g3-cell-label {
padding: 8px;
border-right: 1px solid #ddd;
display: flex;
align-items: center;
justify-content: flex-end;
width: 95px;
flex-shrink: 0;
background: #f9f9f9;
height: 100%;
}
.g3-cell-value {
width: 100%;
}
.g3-actions {
display: flex;
margin-top: 12px;
justify-content: end;
}
:deep(.el-input__wrapper) {
border: none !important;
box-shadow: none !important;
border-radius: 0px;
}
</style>