yangg 2 months ago
parent
commit
c46b07ea4f
43 changed files with 4077 additions and 8 deletions
  1. 2 2
      .env.development
  2. 6 0
      package-lock.json
  3. 1 0
      package.json
  4. 3 3
      src/layout/logo/index.vue
  5. 28 1
      src/settings.ts
  6. 1 0
      src/utils/chinaAreaData.ts
  7. 50 0
      src/views/position/list/api.ts
  8. 76 0
      src/views/position/list/components/RoleDrawer.vue
  9. 186 0
      src/views/position/list/components/RoleMenuBtn.vue
  10. 118 0
      src/views/position/list/components/RoleMenuField.vue
  11. 81 0
      src/views/position/list/components/RoleMenuTree.vue
  12. 35 0
      src/views/position/list/components/RoleUsers.vue
  13. 121 0
      src/views/position/list/components/api.ts
  14. 388 0
      src/views/position/list/crud.tsx
  15. 36 0
      src/views/position/list/index.vue
  16. 37 0
      src/views/position/list/stores/RoleDrawerStores.ts
  17. 24 0
      src/views/position/list/stores/RoleMenuBtnStores.ts
  18. 24 0
      src/views/position/list/stores/RoleMenuFieldStores.ts
  19. 21 0
      src/views/position/list/stores/RoleMenuTreeStores.ts
  20. 24 0
      src/views/position/list/stores/RoleUsersStores.ts
  21. 99 0
      src/views/position/list/types.ts
  22. 50 0
      src/views/questionBank/list/api.ts
  23. 76 0
      src/views/questionBank/list/components/RoleDrawer.vue
  24. 186 0
      src/views/questionBank/list/components/RoleMenuBtn.vue
  25. 118 0
      src/views/questionBank/list/components/RoleMenuField.vue
  26. 81 0
      src/views/questionBank/list/components/RoleMenuTree.vue
  27. 35 0
      src/views/questionBank/list/components/RoleUsers.vue
  28. 121 0
      src/views/questionBank/list/components/api.ts
  29. 429 0
      src/views/questionBank/list/crud.tsx
  30. 36 0
      src/views/questionBank/list/index.vue
  31. 37 0
      src/views/questionBank/list/stores/RoleDrawerStores.ts
  32. 24 0
      src/views/questionBank/list/stores/RoleMenuBtnStores.ts
  33. 24 0
      src/views/questionBank/list/stores/RoleMenuFieldStores.ts
  34. 21 0
      src/views/questionBank/list/stores/RoleMenuTreeStores.ts
  35. 24 0
      src/views/questionBank/list/stores/RoleUsersStores.ts
  36. 99 0
      src/views/questionBank/list/types.ts
  37. 80 0
      src/views/questionBank/positionList/api.ts
  38. 115 0
      src/views/questionBank/positionList/components/DocumentFormCom/index.vue
  39. 128 0
      src/views/questionBank/positionList/components/DocumentTreeCom/index.vue
  40. 487 0
      src/views/questionBank/positionList/crud.tsx
  41. 530 0
      src/views/questionBank/positionList/index.vue
  42. 13 0
      src/views/system/login/component/account.vue
  43. 2 2
      src/views/system/login/index.vue

+ 2 - 2
.env.development

@@ -1,8 +1,8 @@
 # 本地环境
 ENV = 'development'
 
-# 本地环境接口地址
-VITE_API_URL = 'http://121.36.251.245:8083'
+# 本地环境接口地址 121.36.251.245
+VITE_API_URL = 'http://192.168.66.187:8083'
 
 # 是否启用按钮权限
 VITE_PM_ENABLED = true

+ 6 - 0
package-lock.json

@@ -23,6 +23,7 @@
 				"axios": "^1.7.4",
 				"bootstrap": "^5.3.5",
 				"bootstrap-vue-3": "^0.5.1",
+				"china-area-data": "^5.0.1",
 				"countup.js": "^2.8.0",
 				"cropperjs": "^1.6.2",
 				"e-icon-picker": "2.1.1",
@@ -5028,6 +5029,11 @@
 				"url": "https://github.com/chalk/chalk?sponsor=1"
 			}
 		},
+		"node_modules/china-area-data": {
+			"version": "5.0.1",
+			"resolved": "https://registry.npmmirror.com/china-area-data/-/china-area-data-5.0.1.tgz",
+			"integrity": "sha512-BQDPpiv5Nn+018ekcJK2oSD9PAD+E1bvXB0wgabc//dFVS/KvRqCgg0QOEUt3vBkx9XzB5a9BmkJCEZDBxVjVw=="
+		},
 		"node_modules/chokidar": {
 			"version": "4.0.3",
 			"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",

+ 1 - 0
package.json

@@ -24,6 +24,7 @@
 		"axios": "^1.7.4",
 		"bootstrap": "^5.3.5",
 		"bootstrap-vue-3": "^0.5.1",
+		"china-area-data": "^5.0.1",
 		"countup.js": "^2.8.0",
 		"cropperjs": "^1.6.2",
 		"e-icon-picker": "2.1.1",

+ 3 - 3
src/layout/logo/index.vue

@@ -1,7 +1,7 @@
 <template>
 	<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
-		<img :src="siteLogo" class="layout-logo-medium-img" />
-		<span style="font-size: x-large">{{ getSystemConfig['login.site_title'] || themeConfig.globalTitle }}</span>
+		<img :src="siteLogo" class="layout-logo-medium-img"/>
+		<!-- <span style="font-size: x-large">{{ getSystemConfig['login.site_title'] || themeConfig.globalTitle }}</span> -->
 	</div>
 	<div class="layout-logo-size" v-else @click="onThemeConfigChange">
 		<img :src="siteLogo" class="layout-logo-size-img" />
@@ -70,7 +70,7 @@ const siteLogo = computed(() => {
 	}
 
 	&-medium-img {
-		width: 40px;
+		width: 70px;
 		margin-right: 5px;
 	}
 }

+ 28 - 1
src/settings.ts

@@ -48,7 +48,34 @@ export default {
                         transformRes: ({res}: any) => {
                             //将pageRequest的返回数据,转换为fast-crud所需要的格式
                             //return {records,currentPage,pageSize,total};
-                            return {records: res.data, currentPage: res.page, pageSize: res.limit, total: res.total};
+                            // 检查返回的数据结构
+                            if (res && res.data) {
+                                // 如果data.items存在且是数组
+                                if (res.data.items && Array.isArray(res.data.items)) {
+                                    return {
+                                        records: res.data.items,
+                                        currentPage: res.page || 1,
+                                        pageSize: res.limit || 20,
+                                        total: res.data.total || res.total || res.data.items.length
+                                    };
+                                }
+                                // 如果data本身就是数组
+                                else if (Array.isArray(res.data)) {
+                                    return {
+                                        records: res.data,
+                                        currentPage: res.page || 1,
+                                        pageSize: res.limit || 20,
+                                        total: res.total || res.data.length
+                                    };
+                                }
+                                // 其他情况
+                                return {
+                                    records: res.data,
+                                    currentPage: res.page || 1,
+                                    pageSize: res.limit || 20,
+                                    total: res.total || 0
+                                };
+                            }
                         },
                     },
                     form: {

+ 1 - 0
src/utils/chinaAreaData.ts

@@ -0,0 +1 @@
+ 

+ 50 - 0
src/views/position/list/api.ts

@@ -0,0 +1,50 @@
+import { request } from '/@/utils/service';
+import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+import { Session } from '/@/utils/storage';
+
+export const apiPrefix = '/api/job/create';
+
+export function GetPermission() {
+	return request({
+		url: apiPrefix + 'field_permission/',
+		method: 'get',
+	});
+}
+
+export function GetList(query: PageQuery) {
+	return request({
+		url: '/api/job/list',
+		method: 'get',
+		params: {...query,tenant_id:'1'},/* :Session.get('tenant_id').tenant_id ||  */
+	});
+}
+export function GetObj(id: InfoReq) {
+	return request({
+		url: "/api/job/detail" + id,
+		method: 'get',
+	});
+}
+
+export function AddObj(obj: AddReq) {
+	return request({
+		url: "/api/job/create",
+		method: 'post',
+		data: {...obj,tenant_id: '1'},/* Session.get('tenant_id').tenant_id || */
+	});
+}
+
+export function UpdateObj(obj: EditReq) {
+	return request({
+		url: "/api/job/update",/* + obj.id+'/' */
+		method: 'put',
+		data: {...obj,tenant_id: '1'},
+	});
+}
+
+export function DelObj(id: DelReq) {
+	return request({
+		url: '/api/job/delete',/* + id + '/' */
+		method: 'delete',
+		data: { id,tenant_id: '1' },
+	});
+}

+ 76 - 0
src/views/position/list/components/RoleDrawer.vue

@@ -0,0 +1,76 @@
+<template>
+	<el-drawer
+		v-model="RoleDrawer.drawerVisible"
+		title="权限配置"
+		direction="rtl"
+		size="80%"
+		:close-on-click-modal="false"
+		:before-close="RoleDrawer.handleDrawerClose"
+		:destroy-on-close="true"
+	>
+		<template #header>
+			<div>
+				当前授权角色:
+				<el-tag style="margin-right: 20px">{{ RoleDrawer.roleName }}</el-tag>
+				授权人员:
+				<el-button size="small" :icon="UserFilled" @click="handleUsers">{{ RoleDrawer.users.length }}</el-button>
+			</div>
+		</template>
+		<splitpanes class="default-theme" style="height: 100%">
+			<pane min-size="20" size="22">
+				<div class="pane-box">
+					<MenuTreeCom />
+				</div>
+			</pane>
+			<pane min-size="20">
+				<div class="pane-box">
+					<el-tabs v-model="activeName" class="demo-tabs">
+						<el-tab-pane label="接口权限" name="first"><MenuBtnCom /></el-tab-pane>
+						<el-tab-pane label="列字段权限" name="second"><MenuFieldCom /></el-tab-pane>
+					</el-tabs>
+				</div>
+			</pane>
+		</splitpanes>
+	</el-drawer>
+
+	<el-dialog v-model="dialogVisible" title="授权用户" width="700px" :close-on-click-modal="false">
+		<RoleUsersCom />
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { Splitpanes, Pane } from 'splitpanes';
+import 'splitpanes/dist/splitpanes.css';
+import { UserFilled } from '@element-plus/icons-vue';
+import { RoleDrawerStores } from '../stores/RoleDrawerStores';
+import { defineAsyncComponent, ref } from 'vue';
+import { RoleUsersStores } from '../stores/RoleUsersStores';
+
+const MenuTreeCom = defineAsyncComponent(() => import('./RoleMenuTree.vue'));
+const MenuBtnCom = defineAsyncComponent(() => import('./RoleMenuBtn.vue'));
+const MenuFieldCom = defineAsyncComponent(() => import('./RoleMenuField.vue'));
+const RoleUsersCom = defineAsyncComponent(() => import('./RoleUsers.vue'));
+const RoleDrawer = RoleDrawerStores(); // 抽屉参数
+const RoleUsers = RoleUsersStores(); // 角色-用户
+const activeName = ref('first');
+
+const dialogVisible = ref(false);
+
+const handleUsers = () => {
+	dialogVisible.value = true;
+	RoleUsers.get_all_users(); // 获取所有用户
+	RoleUsers.set_right_users(RoleDrawer.$state.users); // 设置已选中用户
+};
+</script>
+
+<style lang="scss" scoped>
+.pane-box {
+	width: 100vw; /* 视口宽度 */
+	height: 100vh; /* 视口高度 */
+	max-width: 100%; /* 确保不超过父元素的宽度 */
+	max-height: 100%; /* 确保不超过父元素的高度 */
+	overflow: auto; /* 当内容超出容器尺寸时显示滚动条 */
+	padding: 10px;
+	background-color: #fff;
+}
+</style>

+ 186 - 0
src/views/position/list/components/RoleMenuBtn.vue

@@ -0,0 +1,186 @@
+<template>
+	<div class="pccm-item" v-if="RoleMenuBtn.$state.length > 0">
+		<div class="menu-form-alert">配置操作功能接口权限,配置数据权限点击小齿轮</div>
+		<el-checkbox v-for="btn in RoleMenuBtn.$state" :key="btn.id" v-model="btn.isCheck" @change="handleCheckChange(btn)">
+			<div class="btn-item">
+				{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }}
+				<span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(btn)">
+					<el-icon>
+						<Setting />
+					</el-icon>
+				</span>
+			</div>
+		</el-checkbox>
+	</div>
+	<el-dialog v-model="dialogVisible" title="数据权限配置" width="400px" :close-on-click-modal="false" :before-close="handleDialogClose">
+		<div class="pc-dialog">
+			<el-select v-model="selectBtn.data_range" @change="handlePermissionRangeChange" placeholder="请选择">
+				<el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label" :value="item.value" />
+			</el-select>
+			<el-tree-select
+				v-show="selectBtn.data_range === 4"
+				node-key="id"
+				v-model="selectBtn.dept"
+				:props="defaultTreeProps"
+				:data="deptData"
+				multiple
+				check-strictly
+				:render-after-expand="false"
+				show-checkbox
+				class="dialog-tree"
+			/>
+		</div>
+		<template #footer>
+			<div>
+				<el-button type="primary" @click="handleDialogConfirm"> 确定</el-button>
+				<el-button @click="handleDialogClose"> 取消</el-button>
+			</div>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref } from 'vue';
+import { RoleDrawerStores } from '../stores/RoleDrawerStores';
+import { RoleMenuBtnStores } from '../stores/RoleMenuBtnStores';
+import { RoleMenuTreeStores } from '../stores/RoleMenuTreeStores';
+import { RoleMenuBtnType } from '../types';
+import { getRoleToDeptAll, setRoleMenuBtn, setRoleMenuBtnDataRange } from './api';
+import XEUtils from 'xe-utils';
+import { ElMessage } from 'element-plus';
+const RoleDrawer = RoleDrawerStores(); // 角色-菜单
+const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单
+const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮
+const dialogVisible = ref(false);
+// 选中的按钮
+const selectBtn = ref<RoleMenuBtnType>({
+	id: 0,
+	menu_btn_pre_id: 0,
+	/** 是否选中 */
+	isCheck: false,
+	/** 按钮名称 */
+	name: '',
+	/** 数据权限范围 */
+	data_range: 0,
+	dept: [],
+});
+/**
+ * 数据权限范围
+ */
+const dataPermissionRange = ref([
+	{ label: '仅本人数据权限', value: 0 },
+	{ label: '本部门及以下数据权限', value: 1 },
+	{ label: '本部门数据权限', value: 2 },
+	{ label: '全部数据权限', value: 3 },
+	{ label: '自定数据权限', value: 4 },
+]);
+/**
+ * 自定义数据权限的部门树配置
+ */
+const defaultTreeProps = {
+	children: 'children',
+	label: 'name',
+	value: 'id',
+};
+
+/**
+ * 自定数据权限下拉选择事件
+ */
+const handlePermissionRangeChange = async (val: number) => {
+	if (val < 4) {
+		selectBtn.value.dept = [];
+	}
+};
+/**
+ * 格式化按钮数据范围
+ */
+const formatDataRange = computed(() => {
+	return function (datarange: number) {
+		const datarangeitem = XEUtils.find(dataPermissionRange.value, (item: any) => {
+			if (item.value === datarange) {
+				return item.label;
+			}
+		});
+		return datarangeitem.label;
+	};
+});
+/**
+ * 勾选按钮
+ */
+const handleCheckChange = async (btn: RoleMenuBtnType) => {
+	const put_data = {
+		isCheck: btn.isCheck,
+		roleId: RoleDrawer.roleId,
+		menuId: RoleMenuTree.id,
+		btnId: btn.id,
+	};
+	const { data, msg } = await setRoleMenuBtn(put_data);
+	RoleMenuBtn.updateState(data);
+	ElMessage({ message: msg, type: 'success' });
+};
+
+/**
+ * 按钮-数据范围确定
+ */
+const handleDialogConfirm = async () => {
+	const { data, msg } = await setRoleMenuBtnDataRange(selectBtn.value);
+	selectBtn.value = data;
+	dialogVisible.value = false;
+	ElMessage({ message: msg, type: 'success' });
+};
+/**
+ * 数据范围关闭
+ */
+const handleDialogClose = () => {
+	dialogVisible.value = false;
+};
+
+/**
+ * 齿轮点击
+ */
+const handleSettingClick = async (btn: RoleMenuBtnType) => {
+	selectBtn.value = btn;
+	dialogVisible.value = true;
+};
+
+/**
+ * 部门数据
+ *
+ */
+const deptData = ref<number[]>([]);
+// 页面打开后获取列表数据
+onMounted(async () => {
+	const res = await getRoleToDeptAll({ role: RoleDrawer.roleId, menu_button: selectBtn.value.id });
+	const depts = XEUtils.toArrayTree(res.data, { parentKey: 'parent', strict: false });
+	deptData.value = depts;
+});
+</script>
+
+<style lang="scss" scoped>
+.pccm-item {
+	margin-bottom: 10px;
+	.menu-form-alert {
+		color: #fff;
+		line-height: 24px;
+		padding: 8px 16px;
+		margin-bottom: 20px;
+		border-radius: 4px;
+		background-color: var(--el-color-primary);
+	}
+}
+// .el-checkbox {
+// 	width: 200px;
+// }
+.btn-item {
+	display: flex;
+	align-items: center;
+	justify-content: center; /* 水平居中 */
+	.el-icon {
+		margin-left: 5px;
+	}
+}
+.dialog-tree {
+	width: 100%;
+	margin-top: 20px;
+}
+</style>

+ 118 - 0
src/views/position/list/components/RoleMenuField.vue

@@ -0,0 +1,118 @@
+<template>
+	<div class="pccm-item" v-if="RoleMenuField.$state.length > 0">
+		<div class="menu-form-alert">
+			<el-button size="small" @click="handleSaveField">保存 </el-button>
+			配置数据列字段权限
+		</div>
+
+		<ul class="columns-list">
+			<li class="columns-head">
+				<div class="width-txt">
+					<span>字段</span>
+				</div>
+				<div v-for="(head, hIndex) in RoleMenuFieldHeader.$state" :key="hIndex" class="width-check">
+					<el-checkbox v-model="head.checked" @change="handleColumnChange($event, head.value, head.disabled)">
+						<span>{{ head.label }}</span>
+					</el-checkbox>
+				</div>
+			</li>
+			<div class="columns-content">
+				<li v-for="(c_item, c_index) in RoleMenuField.$state" :key="c_index" class="columns-item">
+					<div class="width-txt">{{ c_item.title }}</div>
+					<div v-for="(col, cIndex) in RoleMenuFieldHeader.$state" :key="cIndex" class="width-check">
+						<el-checkbox v-model="c_item[col.value]" class="ci-checkout" :disabled="c_item[col.disabled]"></el-checkbox>
+					</div>
+				</li>
+			</div>
+		</ul>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ElMessage } from 'element-plus';
+import { RoleDrawerStores } from '../stores/RoleDrawerStores';
+import { RoleMenuFieldStores, RoleMenuFieldHeaderStores } from '../stores/RoleMenuFieldStores';
+import { setRoleMenuField } from './api';
+const RoleMenuField = RoleMenuFieldStores();
+const RoleMenuFieldHeader = RoleMenuFieldHeaderStores();
+const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
+/** 全选 */
+const handleColumnChange = (val: boolean, btnType: string, disabledType: string) => {
+	for (const iterator of RoleMenuField.$state) {
+		iterator[btnType] = iterator[disabledType] ? iterator[btnType] : val;
+	}
+};
+const handleSaveField = async () => {
+	const res = await setRoleMenuField(RoleDrawer.$state.roleId, RoleMenuField.$state);
+	ElMessage({ message: res.msg, type: 'success' });
+};
+</script>
+
+<style lang="scss" scoped>
+.pccm-item {
+	margin-bottom: 10px;
+	.menu-form-alert {
+		color: #fff;
+		line-height: 24px;
+		padding: 8px 16px;
+		margin-bottom: 20px;
+		border-radius: 4px;
+		background-color: var(--el-color-primary);
+	}
+	.menu-form-btn {
+		margin-left: 10px;
+		height: 40px;
+		padding: 8px 16px;
+		margin-bottom: 20px;
+	}
+
+	.btn-item {
+		display: flex;
+		align-items: center;
+
+		span {
+			margin-left: 5px;
+		}
+	}
+
+	.columns-list {
+		.width-txt {
+			width: 200px;
+		}
+
+		.width-check {
+			width: 100px;
+		}
+
+		.width-icon {
+			cursor: pointer;
+		}
+
+		.columns-head {
+			display: flex;
+			align-items: center;
+			padding: 6px 0;
+			border-bottom: 1px solid #ebeef5;
+			box-sizing: border-box;
+
+			span {
+				font-weight: 900;
+			}
+		}
+		.columns-content {
+			max-height: calc(100vh - 240px); /* 视口高度 */
+			overflow-y: auto; /* 当内容超出高度时显示垂直滚动条 */
+			.columns-item {
+				display: flex;
+				align-items: center;
+				padding: 6px 0;
+				box-sizing: border-box;
+
+				.ci-checkout {
+					height: auto !important;
+				}
+			}
+		}
+	}
+}
+</style>

+ 81 - 0
src/views/position/list/components/RoleMenuTree.vue

@@ -0,0 +1,81 @@
+<template>
+	<el-tree
+		ref="treeRef"
+		:data="menuData"
+		:props="defaultTreeProps"
+		:default-checked-keys="menuDefaultCheckedKeys"
+		@check-change="handleMenuChange"
+		@node-click="handleMenuClick"
+		node-key="id"
+		check-strictly
+		highlight-current
+		show-checkbox
+		default-expand-all
+	>
+	</el-tree>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { RoleDrawerStores } from '../stores/RoleDrawerStores';
+import { RoleMenuTreeStores } from '../stores/RoleMenuTreeStores';
+import { RoleMenuBtnStores } from '../stores/RoleMenuBtnStores';
+import { RoleMenuFieldStores } from '../stores/RoleMenuFieldStores';
+import { RoleMenuFieldHeaderStores } from '../stores/RoleMenuFieldStores';
+import { getRoleMenu, getRoleMenuBtnField, setRoleMenu } from './api';
+import { ElMessage } from 'element-plus';
+import XEUtils from 'xe-utils';
+import { RoleMenuTreeType } from '../types';
+
+const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
+const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单
+const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮
+const RoleMenuField = RoleMenuFieldStores(); // 角色-菜单-列字段
+const RoleMenuFieldHeader = RoleMenuFieldHeaderStores();// 角色-菜单-列字段
+const menuData = ref<RoleMenuTreeType[]>([]); // 菜单列表数据
+const menuDefaultCheckedKeys = ref<(number | string | undefined)[]>([]); // 默认选中的菜单列表
+// 菜单配置
+const defaultTreeProps = {
+	children: 'children',
+	label: 'name',
+	value: 'id',
+};
+
+/**
+ * 菜单复选框选中
+ * @param node:当前节点的 Node 对象
+ * @param checked:布尔值,表示当前节点是否被选中
+ */
+const handleMenuChange = (node: any, checked: boolean) => {
+	setRoleMenu({ roleId: RoleDrawer.roleId, menuId: node.id, isCheck: checked }).then((res: any) => {
+		ElMessage({ message: res.msg, type: 'success' });
+	});
+};
+/**
+ * 菜单点击事件
+ */
+const handleMenuClick = async (selectNode: RoleMenuTreeType) => {
+	if (!selectNode.is_catalog) {
+		RoleMenuTree.setRoleMenuTree(selectNode); // 更新当前选中的菜单
+		// 获取当前菜单的按钮列表
+		const { data } = await getRoleMenuBtnField({
+			roleId: RoleDrawer.roleId,
+			menuId: selectNode.id,
+		});
+		RoleMenuBtn.setState(data.menu_btn); // 更新按钮列表
+		RoleMenuField.setState(data.menu_field); // 更新列字段列表
+	} else {
+		RoleMenuBtn.setState([]); // 更新按钮列表
+		RoleMenuField.setState([]); // 更新列字段列表
+	}
+	RoleMenuFieldHeader.$reset()
+};
+
+// 页面打开后获取列表数据
+onMounted(async () => {
+	menuData.value = await getRoleMenu({ roleId: RoleDrawer.roleId });
+	menuDefaultCheckedKeys.value = XEUtils.toTreeArray(menuData.value)
+		.filter((i) => i.isCheck)
+		.map((i) => i.id);
+});
+</script>

+ 35 - 0
src/views/position/list/components/RoleUsers.vue

@@ -0,0 +1,35 @@
+<template>
+	<el-transfer
+		v-model="RoleUsers.$state.right_users"
+		filterable
+		:titles="['未授权用户', '已授权用户']"
+		:data="RoleUsers.$state.all_users"
+		:props="{
+			key: 'id',
+			label: 'name',
+		}"
+		@change="handleChange"
+	/>
+</template>
+
+<script lang="ts" setup>
+import { ElMessage } from 'element-plus';
+import { RoleDrawerStores } from '../stores/RoleDrawerStores';
+import { RoleUsersStores } from '../stores/RoleUsersStores';
+import { setRoleUsers } from './api';
+const RoleDrawer = RoleDrawerStores(); // 抽屉参数
+const RoleUsers = RoleUsersStores(); // 角色-用户
+
+/**
+ *
+ * @param value 当前右侧选中的用户
+ * @param direction 移动的方向
+ * @param movedKeys 移动的用户
+ */
+const handleChange = (value: number[] | string[], direction: 'left' | 'right', movedKeys: string[] | number[]) => {
+	setRoleUsers(RoleDrawer.$state.roleId, { direction, movedKeys }).then((res:any) => {
+		RoleDrawer.set_state(res.data)
+		ElMessage({ message: res.msg, type: 'success' });
+	});
+};
+</script>

+ 121 - 0
src/views/position/list/components/api.ts

@@ -0,0 +1,121 @@
+import { request } from '/@/utils/service';
+import XEUtils from 'xe-utils';
+/**
+ * 获取 角色-菜单
+ * @param query
+ */
+export function getRoleMenu(query: object) {
+	return request({
+		url: '/api/system/role_menu_button_permission/get_role_menu/',
+		method: 'get',
+		params: query,
+	}).then((res: any) => {
+		return XEUtils.toArrayTree(res.data, { key: 'id', parentKey: 'parent', children: 'children', strict: false });
+	});
+}
+/**
+ * 设置 角色-菜单
+ * @param data
+ * @returns
+ */
+export function setRoleMenu(data: object) {
+	return request({
+		url: '/api/system/role_menu_button_permission/set_role_menu/',
+		method: 'put',
+		data,
+	});
+}
+/**
+ * 获取 角色-菜单-按钮-列字段
+ * @param query
+ */
+export function getRoleMenuBtnField(query: object) {
+	return request({
+		url: '/api/system/role_menu_button_permission/get_role_menu_btn_field/',
+		method: 'get',
+		params: query,
+	});
+}
+
+/**
+ * 设置 角色-菜单-按钮
+ * @param data
+ */
+export function setRoleMenuBtn(data: object) {
+	return request({
+		url: '/api/system/role_menu_button_permission/set_role_menu_btn/',
+		method: 'put',
+		data,
+	});
+}
+
+/**
+ * 设置 角色-菜单-列字段
+ * @param data
+ */
+export function setRoleMenuField(roleId: string | number | undefined, data: object) {
+	return request({
+		url: `/api/system/role_menu_button_permission/${roleId}/set_role_menu_field/`,
+		method: 'put',
+		data,
+	});
+}
+
+/**
+ * 设置 角色-菜单-按钮-数据权限
+ * @param query
+ * @returns
+ */
+export function setRoleMenuBtnDataRange(data: object) {
+	return request({
+		url: '/api/system/role_menu_button_permission/set_role_menu_btn_data_range/',
+		method: 'put',
+		data,
+	});
+}
+
+/**
+ * 获取当前用户角色下所能授权的部门
+ * @param query
+ * @returns
+ */
+export function getRoleToDeptAll(query: object) {
+	return request({
+		url: '/api/system/role_menu_button_permission/role_to_dept_all/',
+		method: 'get',
+		params: query,
+	});
+}
+
+/**
+ * 获取所有用户
+ * @param query
+ * @returns
+ */
+export function getAllUsers() {
+	return request({
+		url: '/api/system/user/',
+		method: 'get',
+		params: { limit: 999 },
+	}).then((res: any) => {
+		return XEUtils.map(res.data, (item: any) => {
+			return {
+				id: item.id,
+				name: item.name,
+			};
+		});
+	});
+}
+
+/**
+ * 设置角色-用户
+ * @param query
+ * @returns
+ */
+export function setRoleUsers(roleId: string | number | undefined, data: object) {
+	return request({
+		url: `/api/system/role/${roleId}/set_role_users/`,
+		method: 'put',
+		data,
+	});
+}

+ 388 - 0
src/views/position/list/crud.tsx

@@ -0,0 +1,388 @@
+import { CreateCrudOptionsProps, CreateCrudOptionsRet, AddReq, DelReq, EditReq, dict, compute } from '@fast-crud/fast-crud';
+import * as api from './api';
+import { dictionary } from '/@/utils/dictionary';
+import { successMessage } from '../../../utils/message';
+import { auth } from '/@/utils/authFunction';
+
+/**
+ *
+ * @param crudExpose:index传递过来的示例
+ * @param context:index传递过来的自定义参数
+ * @returns
+ */
+export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
+	const pageRequest = async (query: any) => {
+		return await api.GetList(query);
+	};
+	const editRequest = async ({ form, row }: EditReq) => {
+		form.id = row.id;
+		return await api.UpdateObj(form);
+	};
+	const delRequest = async ({ row }: DelReq) => {
+		return await api.DelObj(row.id);
+	};
+	const addRequest = async ({ form }: AddReq) => {
+		return await api.AddObj(form);
+	};
+
+	return {
+		crudOptions: {
+			request: {
+				pageRequest,
+				addRequest,
+				editRequest,
+				delRequest,
+			},
+			pagination: {
+				show: true,
+			},
+			actionbar: {
+				buttons: {
+					add: {
+						show: auth('role:Create'),
+					},
+				},
+			},
+			rowHandle: {
+				//固定右侧
+				fixed: 'right',
+				width: 320,
+				buttons: {
+					view: {
+						show: true,
+					},
+					edit: {
+						show: auth('role:Update'),
+					},
+					remove: {
+						show: auth('role:Delete'),
+					},
+					/* permission: {
+						type: 'primary',
+						text: '权限配置',
+						show: auth('role:Permission'),
+						click: (clickContext: any): void => {
+							const { row } = clickContext;
+							context.RoleDrawer.handleDrawerOpen(row);
+							context.RoleMenuBtn.setState([]);
+							context.RoleMenuField.setState([]);
+						},
+					}, */
+				},
+			},
+			form: {
+				col: { span: 24 },
+				labelWidth: '100px',
+				wrapper: {
+					is: 'el-dialog',
+					width: '600px',
+				},
+			},
+			columns: {
+				/* _index: {
+					title: '序号',
+					form: { show: false },
+					column: {
+						type: 'index',
+						align: 'center',
+						width: '70px',
+						columnSetDisabled: true, //禁止在列设置中选择
+					},
+				}, */
+				id: {
+					title: 'ID',
+					column: { show: false },
+					search: { show: false },
+					form: { show: false },
+				},
+				title: {
+					title: '职位名称',
+					search: { show: true },
+					column: {
+						minWidth: 120,
+						sortable: 'custom',
+					},
+					form: {
+						rules: [{ required: true, message: '角色名称必填' }],
+						component: {
+							placeholder: '请输入角色名称',
+						},
+					},
+				},
+				/* key: {
+					title: '权限标识',
+					search: { show: false },
+					column: {
+						minWidth: 120,
+						sortable: 'custom',
+						columnSetDisabled: true,
+					},
+					form: {
+						rules: [{ required: true, message: '权限标识必填' }],
+						component: {
+							placeholder: '输入权限标识',
+						},
+					},
+					valueBuilder(context) {
+						const { row, key } = context;
+						return row[key];
+					},
+				},
+				sort: {
+					title: '排序',
+					search: { show: false },
+					type: 'number',
+					column: {
+						minWidth: 90,
+						sortable: 'custom',
+					},
+					form: {
+						rules: [{ required: true, message: '排序必填' }],
+						value: 1,
+					},
+				},*/
+				status: {
+					title: '状态',
+					search: { show: true },
+					type: 'dict-select',
+					column: {
+						width: 100,
+						/* component: {
+							name: 'fs-dict-label',
+							props: {
+								dict: dict({
+									data: [
+										{ value: 0, label: '草稿' },
+										{ value: 1, label: '已发布' },
+										{ value: 2, label: '已结束' }
+									]
+								})
+							}
+						}, */
+					},
+					dict: dict({
+						data: [
+							{ value: 0, label: '草稿' },
+							{ value: 1, label: '已发布' },
+							{ value: 2, label: '已结束' }
+						]
+					}),
+					form: {
+						rules: [{ required: true, message: '状态必填' }],
+						component: {
+							placeholder: '请选择状态',
+						},
+					},
+				},
+				salary_range: {
+					title: '薪资范围',
+					search: { show: true },
+					column: {
+						minWidth: 100,
+					},
+					form: {
+						rules: [{ required: true, message: '薪资范围必填' }],
+						component: {
+							placeholder: '请输入薪资范围',
+						},
+					},
+				},
+				location: {
+					title: '工作地点',
+					search: { show: true },
+					column: {
+						minWidth: 120,
+						formatter: ({ row }) => {
+							if (Array.isArray(row.location)) {
+								return row.location.join(' ');
+							}
+							if (row.province && row.city) {
+								return row.province + ' ' + row.city + (row.district ? ' ' + row.district : '');
+							}
+							return row.location || '';
+						}
+					},
+					form: {
+						component: {
+							name: 'el-cascader',
+							props: {
+								options: [
+									{
+										value: '上海市',
+										label: '上海市',
+										children: [
+											{
+												value: '浦东新区',
+												label: '浦东新区',
+												children: [
+													{ value: '张江', label: '张江' },
+													{ value: '金桥', label: '金桥' },
+													{ value: '陆家嘴', label: '陆家嘴' }
+												]
+											},
+											{
+												value: '徐汇区',
+												label: '徐汇区',
+												children: [
+													{ value: '漕河泾', label: '漕河泾' },
+													{ value: '徐家汇', label: '徐家汇' }
+												]
+											},
+											// 其他区域...
+										]
+									},
+									{
+										value: '北京市',
+										label: '北京市',
+										children: [
+											{
+												value: '海淀区',
+												label: '海淀区',
+												children: [
+													{ value: '中关村', label: '中关村' },
+													{ value: '上地', label: '上地' }
+												]
+											},
+											{
+												value: '朝阳区',
+												label: '朝阳区',
+												children: [
+													{ value: 'CBD', label: 'CBD' },
+													{ value: '望京', label: '望京' }
+												]
+											},
+											// 其他区域...
+										]
+									},
+									// 其他省市...
+								],
+								props: {
+									expandTrigger: 'hover',
+									checkStrictly: false
+								},
+								placeholder: '请选择工作地点',
+								clearable: true,
+								showAllLevels: true
+							},
+							on: {
+								change: (value) => {
+									console.log('地址选择变化:', value);
+								}
+							}
+						},
+						valueResolve: (form) => {
+							if (form.location && Array.isArray(form.location) && form.location.length > 0) {
+								form.province = form.location[0];
+								form.city = form.location[1] || '';
+								form.district = form.location[2] || '';
+								form.location = form.location.join(' ');
+							}
+						},
+						valueBuilder: (form) => {
+							if (form.province && form.city) {
+								form.location = [form.province, form.city];
+								if (form.district) {
+									form.location.push(form.district);
+								}
+							} else if (typeof form.location === 'string' && form.location) {
+								const parts = form.location.split(' ');
+								if (parts.length > 0) {
+									form.location = parts;
+								}
+							}
+						}
+					},
+				},
+				department: {
+					title: '所属部门',
+					search: { show: true },
+					column: {
+						minWidth: 100,
+					},
+					form: {
+						rules: [{ required: true, message: '所属部门必填' }],
+						component: {
+							placeholder: '请输入所属部门',
+						},
+					},
+				},
+				job_type: {
+					title: '职位类型',
+					search: { show: true },
+					type: 'dict-select',
+					column: {
+						minWidth: 100,
+					},
+					dict: dict({
+						data: [
+							{ value: 0, label: '全职' },
+							{ value: 1, label: '兼职' },
+							{ value: 2, label: '实习' }
+						]
+					}),
+					form: {
+						rules: [{ required: true, message: '职位类型必填' }],
+					},
+				},
+				end_date: {
+					title: '截止日期',
+					type: 'datetime',
+					search: { show: true },
+					column: {
+						minWidth: 150,
+						sortable: 'custom',
+					},
+					form: {
+						rules: [{ required: true, message: '截止日期必填' }],
+						component: {
+							type: 'datetime',
+							valueFormat: 'YYYY-MM-DD HH:mm:ss',
+							placeholder: '请选择截止日期',
+						},
+					},
+				},
+				requirements: {
+					title: '职位要求',
+					search: { show: false },
+					column: {
+						minWidth: 120,
+					},
+					form: {
+						component: {
+							type: 'textarea',
+							rows: 4,
+						},
+					},
+				},
+				description: {
+					title: '职位描述',
+					search: { show: false },
+					column: {
+						minWidth: 120,
+					},
+					form: {
+						component: {
+							type: 'textarea',
+							rows: 4,
+						},
+					},
+				},
+				province: {
+					title: '省份',
+					column: { show: false },
+					form: { show: false }
+				},
+				city: {
+					title: '城市',
+					column: { show: false },
+					form: { show: false }
+				},
+				district: {
+					title: '区域',
+					column: { show: false },
+					form: { show: false }
+				},
+			},
+		},
+	};
+};

+ 36 - 0
src/views/position/list/index.vue

@@ -0,0 +1,36 @@
+<template>
+	<fs-page>
+		<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
+		<PermissionDrawerCom />
+	</fs-page>
+</template>
+
+<script lang="ts" setup name="role">
+import { defineAsyncComponent, onMounted } from 'vue';
+import { useFs } from '@fast-crud/fast-crud';
+import { createCrudOptions } from './crud';
+import { RoleDrawerStores } from './stores/RoleDrawerStores';
+import { RoleMenuBtnStores } from './stores/RoleMenuBtnStores';
+import { RoleMenuFieldStores } from './stores/RoleMenuFieldStores';
+import { RoleUsersStores } from './stores/RoleUsersStores';
+
+const PermissionDrawerCom = defineAsyncComponent(() => import('./components/RoleDrawer.vue'));
+
+const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
+const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单
+const RoleMenuField = RoleMenuFieldStores();// 角色-菜单-字段
+const RoleUsers = RoleUsersStores();// 角色-用户
+const { crudBinding, crudRef, crudExpose } = useFs({
+	createCrudOptions,
+	context: { RoleDrawer, RoleMenuBtn, RoleMenuField },
+});
+
+// 页面打开后获取列表数据
+onMounted(async () => {
+	// 刷新
+	crudExpose.doRefresh();
+	// 获取全部用户
+	RoleUsers.get_all_users();
+
+});
+</script>

+ 37 - 0
src/views/position/list/stores/RoleDrawerStores.ts

@@ -0,0 +1,37 @@
+import { defineStore } from 'pinia';
+import { RoleDrawerType } from '../types';
+/**
+ * 权限配置:抽屉
+ */
+const initialState: RoleDrawerType = {
+	drawerVisible: false,
+	roleId: undefined,
+	roleName: undefined,
+	users: [],
+};
+
+export const RoleDrawerStores = defineStore('RoleDrawerStores', {
+	state: (): RoleDrawerType => ({
+		...initialState,
+	}),
+	actions: {
+		/**
+		 * 打开权限修改抽屉
+		 */
+		handleDrawerOpen(row: any) {
+			this.drawerVisible = true;
+			this.set_state(row);
+		},
+		set_state(row: any) {
+			this.roleName = row.name;
+			this.roleId = row.id;
+			this.users = row.users;
+		},
+		/**
+		 * 关闭权限修改抽屉
+		 */
+		handleDrawerClose() {
+			Object.assign(this.$state, initialState);
+		},
+	},
+});

+ 24 - 0
src/views/position/list/stores/RoleMenuBtnStores.ts

@@ -0,0 +1,24 @@
+import { defineStore } from 'pinia';
+import { RoleMenuBtnType } from '../types';
+/**
+ * 权限配置:角色-菜单-按钮
+ */
+
+export const RoleMenuBtnStores = defineStore('RoleMenuBtnStores', {
+	state: (): RoleMenuBtnType[] => [],
+	actions: {
+		/**
+		 * 初始化
+		 */
+		setState(data: RoleMenuBtnType[]) {
+			this.$state = data;
+			this.$state.length = data.length;
+		},
+		updateState(data: RoleMenuBtnType) {
+			const index = this.$state.findIndex((item) => item.id === data.id);
+			if (index !== -1) {
+				this.$state[index] = data;
+			}
+		},
+	},
+});

+ 24 - 0
src/views/position/list/stores/RoleMenuFieldStores.ts

@@ -0,0 +1,24 @@
+import { defineStore } from 'pinia';
+import { RoleMenuFieldType, RoleMenuFieldHeaderType } from '../types';
+/**
+ * 权限配置:角色-菜单-列字段
+ */
+
+export const RoleMenuFieldStores = defineStore('RoleMenuFieldStores', {
+	state: (): RoleMenuFieldType[] => [],
+	actions: {
+		/** 重置 */
+		setState(data: RoleMenuFieldType[]) {
+			this.$state = data;
+			this.$state.length = data.length;
+		},
+	},
+});
+
+export const RoleMenuFieldHeaderStores = defineStore('RoleMenuFieldHeaderStores', {
+	state: (): RoleMenuFieldHeaderType[] => [
+		{ value: 'is_create', label: '新增可见', disabled: 'disabled_create', checked: false },
+		{ value: 'is_update', label: '编辑可见', disabled: 'disabled_update', checked: false },
+		{ value: 'is_query', label: '列表可见', disabled: 'disabled_query', checked: false },
+	],
+});

+ 21 - 0
src/views/position/list/stores/RoleMenuTreeStores.ts

@@ -0,0 +1,21 @@
+import { defineStore } from 'pinia';
+import { RoleMenuTreeType } from '../types';
+/**
+ * 权限抽屉:角色-菜单
+ */
+
+export const RoleMenuTreeStores = defineStore('RoleMenuTreeStores', {
+	state: (): RoleMenuTreeType => ({
+		id: 0,
+		parent: 0,
+		name: '',
+		isCheck: false,
+		is_catalog: false,
+	}),
+	actions: {
+		/** 赋值 */
+		setRoleMenuTree(data: RoleMenuTreeType) {
+			this.$state = data;
+		},
+	},
+});

+ 24 - 0
src/views/position/list/stores/RoleUsersStores.ts

@@ -0,0 +1,24 @@
+import { defineStore } from 'pinia';
+import { RoleUsersType } from '../types';
+import { getAllUsers } from '../components/api';
+import XEUtils from 'xe-utils';
+/**
+ * 权限抽屉:角色-用户
+ */
+
+export const RoleUsersStores = defineStore('RoleUsersStores', {
+	state: (): RoleUsersType => ({
+		all_users: [],
+		right_users: [],
+	}),
+	actions: {
+		get_all_users() {
+			getAllUsers().then((res: any) => {
+				this.$state.all_users = res;
+			});
+		},
+		set_right_users(users: any) {
+			this.$state.right_users = XEUtils.map(users, (item: any) => item.id);
+		},
+	},
+});

+ 99 - 0
src/views/position/list/types.ts

@@ -0,0 +1,99 @@
+/**角色列表数据类型 */
+export interface RoleItemType {
+	id: string | number;
+	modifier_name: string;
+	creator_name: string;
+	create_datetime: string;
+	update_datetime: string;
+	description: string;
+	modifier: string;
+	dept_belong_id: string;
+	name: string;
+	key: string;
+	sort: number;
+	status: boolean;
+	admin: boolean;
+	creator: string;
+}
+
+export interface UsersType {
+	id: string | number;
+	name: string;
+}
+export interface RoleUsersType {
+	all_users: UsersType[];
+	right_users: UsersType[];
+}
+
+/**
+ * 权限配置 抽屉组件参数数据类型
+ */
+export interface RoleDrawerType {
+	/** 是否显示抽屉*/
+	drawerVisible: boolean;
+	/** 角色id*/
+	roleId: string | number | undefined;
+	/** 角色名称*/
+	roleName: string | undefined;
+	/** 用户*/
+	users: UsersType[];
+}
+
+/**
+ * 菜单数据类型
+ */
+export interface RoleMenuTreeType {
+	id: number | string | undefined;
+	/** 父级id */
+	parent: number | string | undefined;
+	name: string;
+	/** 是否选中 */
+	isCheck: boolean;
+	/** 是否是目录 */
+	is_catalog: boolean;
+}
+/**
+ * 菜单-按钮数据类型
+ */
+export interface RoleMenuBtnType {
+	id: string | number;
+	menu_btn_pre_id: string | number;
+	/** 是否选中 */
+	isCheck: boolean;
+	/** 按钮名称 */
+	name: string;
+	/** 数据权限范围 */
+	data_range: number | null;
+	/** 自定义部门 */
+	dept: number[];
+}
+
+/**
+ * 菜单-列字段数据类型
+ */
+export interface RoleMenuFieldType {
+	id: string | number | boolean;
+	/** 模型表字段名 */
+	field_name: string;
+	/** 字段显示名	*/
+	title: string;
+	/** 是否可查询 */
+	is_query: boolean;
+	/** 是否可创建 */
+	is_create: boolean;
+	/** 是否可更新 */
+	is_update: boolean;
+	[key: string]: string | number | boolean;
+}
+/**
+ * 菜单-列字段-标题数据类型
+ */
+export interface RoleMenuFieldHeaderType {
+	value: string;
+	/** 模型表字段名 */
+	label: string;
+	/** 字段显示名	*/
+	disabled: string;
+	/** 是否可查询 */
+	checked: boolean;
+}

+ 50 - 0
src/views/questionBank/list/api.ts

@@ -0,0 +1,50 @@
+import { request } from '/@/utils/service';
+import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+import { Session } from '/@/utils/storage';
+
+export const apiPrefix = '/system/interview_question/create';
+
+export function GetPermission() {
+	return request({
+		url: apiPrefix + 'field_permission/',
+		method: 'get',
+	});
+}
+
+export function GetList(query: PageQuery) {
+	return request({
+		url: '/system/interview_question/list',
+		method: 'get',
+		params: {...query},/* :Session.get('tenant_id').tenant_id || ,tenant_id:'1' */
+	});
+}
+export function GetObj(id: InfoReq) {
+	return request({
+		url: "/system/interview_question/detail" + id,
+		method: 'get',
+	});
+}
+
+export function AddObj(obj: AddReq) {
+	return request({
+		url: "/system/interview_question/create",
+		method: 'post',
+		data: {...obj,tenant_id: '1'},/* Session.get('tenant_id').tenant_id ||,tenant_id: '1' */
+	});
+}
+
+export function UpdateObj(obj: EditReq) {
+	return request({
+		url: "/system/interview_question/update",/* + obj.id+'/' */
+		method: 'put',
+		data: {...obj,tenant_id: '1'},
+	});
+}
+
+export function DelObj(id: DelReq) {
+	return request({
+		url: '/system/interview_question/delete',/* + id + '/' ,tenant_id: '1' */
+		method: 'delete',
+		data: { id},
+	});
+}

+ 76 - 0
src/views/questionBank/list/components/RoleDrawer.vue

@@ -0,0 +1,76 @@
+<template>
+	<el-drawer
+		v-model="RoleDrawer.drawerVisible"
+		title="权限配置"
+		direction="rtl"
+		size="80%"
+		:close-on-click-modal="false"
+		:before-close="RoleDrawer.handleDrawerClose"
+		:destroy-on-close="true"
+	>
+		<template #header>
+			<div>
+				当前授权角色:
+				<el-tag style="margin-right: 20px">{{ RoleDrawer.roleName }}</el-tag>
+				授权人员:
+				<el-button size="small" :icon="UserFilled" @click="handleUsers">{{ RoleDrawer.users.length }}</el-button>
+			</div>
+		</template>
+		<splitpanes class="default-theme" style="height: 100%">
+			<pane min-size="20" size="22">
+				<div class="pane-box">
+					<MenuTreeCom />
+				</div>
+			</pane>
+			<pane min-size="20">
+				<div class="pane-box">
+					<el-tabs v-model="activeName" class="demo-tabs">
+						<el-tab-pane label="接口权限" name="first"><MenuBtnCom /></el-tab-pane>
+						<el-tab-pane label="列字段权限" name="second"><MenuFieldCom /></el-tab-pane>
+					</el-tabs>
+				</div>
+			</pane>
+		</splitpanes>
+	</el-drawer>
+
+	<el-dialog v-model="dialogVisible" title="授权用户" width="700px" :close-on-click-modal="false">
+		<RoleUsersCom />
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { Splitpanes, Pane } from 'splitpanes';
+import 'splitpanes/dist/splitpanes.css';
+import { UserFilled } from '@element-plus/icons-vue';
+import { RoleDrawerStores } from '../stores/RoleDrawerStores';
+import { defineAsyncComponent, ref } from 'vue';
+import { RoleUsersStores } from '../stores/RoleUsersStores';
+
+const MenuTreeCom = defineAsyncComponent(() => import('./RoleMenuTree.vue'));
+const MenuBtnCom = defineAsyncComponent(() => import('./RoleMenuBtn.vue'));
+const MenuFieldCom = defineAsyncComponent(() => import('./RoleMenuField.vue'));
+const RoleUsersCom = defineAsyncComponent(() => import('./RoleUsers.vue'));
+const RoleDrawer = RoleDrawerStores(); // 抽屉参数
+const RoleUsers = RoleUsersStores(); // 角色-用户
+const activeName = ref('first');
+
+const dialogVisible = ref(false);
+
+const handleUsers = () => {
+	dialogVisible.value = true;
+	RoleUsers.get_all_users(); // 获取所有用户
+	RoleUsers.set_right_users(RoleDrawer.$state.users); // 设置已选中用户
+};
+</script>
+
+<style lang="scss" scoped>
+.pane-box {
+	width: 100vw; /* 视口宽度 */
+	height: 100vh; /* 视口高度 */
+	max-width: 100%; /* 确保不超过父元素的宽度 */
+	max-height: 100%; /* 确保不超过父元素的高度 */
+	overflow: auto; /* 当内容超出容器尺寸时显示滚动条 */
+	padding: 10px;
+	background-color: #fff;
+}
+</style>

+ 186 - 0
src/views/questionBank/list/components/RoleMenuBtn.vue

@@ -0,0 +1,186 @@
+<template>
+	<div class="pccm-item" v-if="RoleMenuBtn.$state.length > 0">
+		<div class="menu-form-alert">配置操作功能接口权限,配置数据权限点击小齿轮</div>
+		<el-checkbox v-for="btn in RoleMenuBtn.$state" :key="btn.id" v-model="btn.isCheck" @change="handleCheckChange(btn)">
+			<div class="btn-item">
+				{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }}
+				<span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(btn)">
+					<el-icon>
+						<Setting />
+					</el-icon>
+				</span>
+			</div>
+		</el-checkbox>
+	</div>
+	<el-dialog v-model="dialogVisible" title="数据权限配置" width="400px" :close-on-click-modal="false" :before-close="handleDialogClose">
+		<div class="pc-dialog">
+			<el-select v-model="selectBtn.data_range" @change="handlePermissionRangeChange" placeholder="请选择">
+				<el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label" :value="item.value" />
+			</el-select>
+			<el-tree-select
+				v-show="selectBtn.data_range === 4"
+				node-key="id"
+				v-model="selectBtn.dept"
+				:props="defaultTreeProps"
+				:data="deptData"
+				multiple
+				check-strictly
+				:render-after-expand="false"
+				show-checkbox
+				class="dialog-tree"
+			/>
+		</div>
+		<template #footer>
+			<div>
+				<el-button type="primary" @click="handleDialogConfirm"> 确定</el-button>
+				<el-button @click="handleDialogClose"> 取消</el-button>
+			</div>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref } from 'vue';
+import { RoleDrawerStores } from '../stores/RoleDrawerStores';
+import { RoleMenuBtnStores } from '../stores/RoleMenuBtnStores';
+import { RoleMenuTreeStores } from '../stores/RoleMenuTreeStores';
+import { RoleMenuBtnType } from '../types';
+import { getRoleToDeptAll, setRoleMenuBtn, setRoleMenuBtnDataRange } from './api';
+import XEUtils from 'xe-utils';
+import { ElMessage } from 'element-plus';
+const RoleDrawer = RoleDrawerStores(); // 角色-菜单
+const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单
+const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮
+const dialogVisible = ref(false);
+// 选中的按钮
+const selectBtn = ref<RoleMenuBtnType>({
+	id: 0,
+	menu_btn_pre_id: 0,
+	/** 是否选中 */
+	isCheck: false,
+	/** 按钮名称 */
+	name: '',
+	/** 数据权限范围 */
+	data_range: 0,
+	dept: [],
+});
+/**
+ * 数据权限范围
+ */
+const dataPermissionRange = ref([
+	{ label: '仅本人数据权限', value: 0 },
+	{ label: '本部门及以下数据权限', value: 1 },
+	{ label: '本部门数据权限', value: 2 },
+	{ label: '全部数据权限', value: 3 },
+	{ label: '自定数据权限', value: 4 },
+]);
+/**
+ * 自定义数据权限的部门树配置
+ */
+const defaultTreeProps = {
+	children: 'children',
+	label: 'name',
+	value: 'id',
+};
+
+/**
+ * 自定数据权限下拉选择事件
+ */
+const handlePermissionRangeChange = async (val: number) => {
+	if (val < 4) {
+		selectBtn.value.dept = [];
+	}
+};
+/**
+ * 格式化按钮数据范围
+ */
+const formatDataRange = computed(() => {
+	return function (datarange: number) {
+		const datarangeitem = XEUtils.find(dataPermissionRange.value, (item: any) => {
+			if (item.value === datarange) {
+				return item.label;
+			}
+		});
+		return datarangeitem.label;
+	};
+});
+/**
+ * 勾选按钮
+ */
+const handleCheckChange = async (btn: RoleMenuBtnType) => {
+	const put_data = {
+		isCheck: btn.isCheck,
+		roleId: RoleDrawer.roleId,
+		menuId: RoleMenuTree.id,
+		btnId: btn.id,
+	};
+	const { data, msg } = await setRoleMenuBtn(put_data);
+	RoleMenuBtn.updateState(data);
+	ElMessage({ message: msg, type: 'success' });
+};
+
+/**
+ * 按钮-数据范围确定
+ */
+const handleDialogConfirm = async () => {
+	const { data, msg } = await setRoleMenuBtnDataRange(selectBtn.value);
+	selectBtn.value = data;
+	dialogVisible.value = false;
+	ElMessage({ message: msg, type: 'success' });
+};
+/**
+ * 数据范围关闭
+ */
+const handleDialogClose = () => {
+	dialogVisible.value = false;
+};
+
+/**
+ * 齿轮点击
+ */
+const handleSettingClick = async (btn: RoleMenuBtnType) => {
+	selectBtn.value = btn;
+	dialogVisible.value = true;
+};
+
+/**
+ * 部门数据
+ *
+ */
+const deptData = ref<number[]>([]);
+// 页面打开后获取列表数据
+onMounted(async () => {
+	const res = await getRoleToDeptAll({ role: RoleDrawer.roleId, menu_button: selectBtn.value.id });
+	const depts = XEUtils.toArrayTree(res.data, { parentKey: 'parent', strict: false });
+	deptData.value = depts;
+});
+</script>
+
+<style lang="scss" scoped>
+.pccm-item {
+	margin-bottom: 10px;
+	.menu-form-alert {
+		color: #fff;
+		line-height: 24px;
+		padding: 8px 16px;
+		margin-bottom: 20px;
+		border-radius: 4px;
+		background-color: var(--el-color-primary);
+	}
+}
+// .el-checkbox {
+// 	width: 200px;
+// }
+.btn-item {
+	display: flex;
+	align-items: center;
+	justify-content: center; /* 水平居中 */
+	.el-icon {
+		margin-left: 5px;
+	}
+}
+.dialog-tree {
+	width: 100%;
+	margin-top: 20px;
+}
+</style>

+ 118 - 0
src/views/questionBank/list/components/RoleMenuField.vue

@@ -0,0 +1,118 @@
+<template>
+	<div class="pccm-item" v-if="RoleMenuField.$state.length > 0">
+		<div class="menu-form-alert">
+			<el-button size="small" @click="handleSaveField">保存 </el-button>
+			配置数据列字段权限
+		</div>
+
+		<ul class="columns-list">
+			<li class="columns-head">
+				<div class="width-txt">
+					<span>字段</span>
+				</div>
+				<div v-for="(head, hIndex) in RoleMenuFieldHeader.$state" :key="hIndex" class="width-check">
+					<el-checkbox v-model="head.checked" @change="handleColumnChange($event, head.value, head.disabled)">
+						<span>{{ head.label }}</span>
+					</el-checkbox>
+				</div>
+			</li>
+			<div class="columns-content">
+				<li v-for="(c_item, c_index) in RoleMenuField.$state" :key="c_index" class="columns-item">
+					<div class="width-txt">{{ c_item.title }}</div>
+					<div v-for="(col, cIndex) in RoleMenuFieldHeader.$state" :key="cIndex" class="width-check">
+						<el-checkbox v-model="c_item[col.value]" class="ci-checkout" :disabled="c_item[col.disabled]"></el-checkbox>
+					</div>
+				</li>
+			</div>
+		</ul>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ElMessage } from 'element-plus';
+import { RoleDrawerStores } from '../stores/RoleDrawerStores';
+import { RoleMenuFieldStores, RoleMenuFieldHeaderStores } from '../stores/RoleMenuFieldStores';
+import { setRoleMenuField } from './api';
+const RoleMenuField = RoleMenuFieldStores();
+const RoleMenuFieldHeader = RoleMenuFieldHeaderStores();
+const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
+/** 全选 */
+const handleColumnChange = (val: boolean, btnType: string, disabledType: string) => {
+	for (const iterator of RoleMenuField.$state) {
+		iterator[btnType] = iterator[disabledType] ? iterator[btnType] : val;
+	}
+};
+const handleSaveField = async () => {
+	const res = await setRoleMenuField(RoleDrawer.$state.roleId, RoleMenuField.$state);
+	ElMessage({ message: res.msg, type: 'success' });
+};
+</script>
+
+<style lang="scss" scoped>
+.pccm-item {
+	margin-bottom: 10px;
+	.menu-form-alert {
+		color: #fff;
+		line-height: 24px;
+		padding: 8px 16px;
+		margin-bottom: 20px;
+		border-radius: 4px;
+		background-color: var(--el-color-primary);
+	}
+	.menu-form-btn {
+		margin-left: 10px;
+		height: 40px;
+		padding: 8px 16px;
+		margin-bottom: 20px;
+	}
+
+	.btn-item {
+		display: flex;
+		align-items: center;
+
+		span {
+			margin-left: 5px;
+		}
+	}
+
+	.columns-list {
+		.width-txt {
+			width: 200px;
+		}
+
+		.width-check {
+			width: 100px;
+		}
+
+		.width-icon {
+			cursor: pointer;
+		}
+
+		.columns-head {
+			display: flex;
+			align-items: center;
+			padding: 6px 0;
+			border-bottom: 1px solid #ebeef5;
+			box-sizing: border-box;
+
+			span {
+				font-weight: 900;
+			}
+		}
+		.columns-content {
+			max-height: calc(100vh - 240px); /* 视口高度 */
+			overflow-y: auto; /* 当内容超出高度时显示垂直滚动条 */
+			.columns-item {
+				display: flex;
+				align-items: center;
+				padding: 6px 0;
+				box-sizing: border-box;
+
+				.ci-checkout {
+					height: auto !important;
+				}
+			}
+		}
+	}
+}
+</style>

+ 81 - 0
src/views/questionBank/list/components/RoleMenuTree.vue

@@ -0,0 +1,81 @@
+<template>
+	<el-tree
+		ref="treeRef"
+		:data="menuData"
+		:props="defaultTreeProps"
+		:default-checked-keys="menuDefaultCheckedKeys"
+		@check-change="handleMenuChange"
+		@node-click="handleMenuClick"
+		node-key="id"
+		check-strictly
+		highlight-current
+		show-checkbox
+		default-expand-all
+	>
+	</el-tree>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { RoleDrawerStores } from '../stores/RoleDrawerStores';
+import { RoleMenuTreeStores } from '../stores/RoleMenuTreeStores';
+import { RoleMenuBtnStores } from '../stores/RoleMenuBtnStores';
+import { RoleMenuFieldStores } from '../stores/RoleMenuFieldStores';
+import { RoleMenuFieldHeaderStores } from '../stores/RoleMenuFieldStores';
+import { getRoleMenu, getRoleMenuBtnField, setRoleMenu } from './api';
+import { ElMessage } from 'element-plus';
+import XEUtils from 'xe-utils';
+import { RoleMenuTreeType } from '../types';
+
+const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
+const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单
+const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮
+const RoleMenuField = RoleMenuFieldStores(); // 角色-菜单-列字段
+const RoleMenuFieldHeader = RoleMenuFieldHeaderStores();// 角色-菜单-列字段
+const menuData = ref<RoleMenuTreeType[]>([]); // 菜单列表数据
+const menuDefaultCheckedKeys = ref<(number | string | undefined)[]>([]); // 默认选中的菜单列表
+// 菜单配置
+const defaultTreeProps = {
+	children: 'children',
+	label: 'name',
+	value: 'id',
+};
+
+/**
+ * 菜单复选框选中
+ * @param node:当前节点的 Node 对象
+ * @param checked:布尔值,表示当前节点是否被选中
+ */
+const handleMenuChange = (node: any, checked: boolean) => {
+	setRoleMenu({ roleId: RoleDrawer.roleId, menuId: node.id, isCheck: checked }).then((res: any) => {
+		ElMessage({ message: res.msg, type: 'success' });
+	});
+};
+/**
+ * 菜单点击事件
+ */
+const handleMenuClick = async (selectNode: RoleMenuTreeType) => {
+	if (!selectNode.is_catalog) {
+		RoleMenuTree.setRoleMenuTree(selectNode); // 更新当前选中的菜单
+		// 获取当前菜单的按钮列表
+		const { data } = await getRoleMenuBtnField({
+			roleId: RoleDrawer.roleId,
+			menuId: selectNode.id,
+		});
+		RoleMenuBtn.setState(data.menu_btn); // 更新按钮列表
+		RoleMenuField.setState(data.menu_field); // 更新列字段列表
+	} else {
+		RoleMenuBtn.setState([]); // 更新按钮列表
+		RoleMenuField.setState([]); // 更新列字段列表
+	}
+	RoleMenuFieldHeader.$reset()
+};
+
+// 页面打开后获取列表数据
+onMounted(async () => {
+	menuData.value = await getRoleMenu({ roleId: RoleDrawer.roleId });
+	menuDefaultCheckedKeys.value = XEUtils.toTreeArray(menuData.value)
+		.filter((i) => i.isCheck)
+		.map((i) => i.id);
+});
+</script>

+ 35 - 0
src/views/questionBank/list/components/RoleUsers.vue

@@ -0,0 +1,35 @@
+<template>
+	<el-transfer
+		v-model="RoleUsers.$state.right_users"
+		filterable
+		:titles="['未授权用户', '已授权用户']"
+		:data="RoleUsers.$state.all_users"
+		:props="{
+			key: 'id',
+			label: 'name',
+		}"
+		@change="handleChange"
+	/>
+</template>
+
+<script lang="ts" setup>
+import { ElMessage } from 'element-plus';
+import { RoleDrawerStores } from '../stores/RoleDrawerStores';
+import { RoleUsersStores } from '../stores/RoleUsersStores';
+import { setRoleUsers } from './api';
+const RoleDrawer = RoleDrawerStores(); // 抽屉参数
+const RoleUsers = RoleUsersStores(); // 角色-用户
+
+/**
+ *
+ * @param value 当前右侧选中的用户
+ * @param direction 移动的方向
+ * @param movedKeys 移动的用户
+ */
+const handleChange = (value: number[] | string[], direction: 'left' | 'right', movedKeys: string[] | number[]) => {
+	setRoleUsers(RoleDrawer.$state.roleId, { direction, movedKeys }).then((res:any) => {
+		RoleDrawer.set_state(res.data)
+		ElMessage({ message: res.msg, type: 'success' });
+	});
+};
+</script>

+ 121 - 0
src/views/questionBank/list/components/api.ts

@@ -0,0 +1,121 @@
+import { request } from '/@/utils/service';
+import XEUtils from 'xe-utils';
+/**
+ * 获取 角色-菜单
+ * @param query
+ */
+export function getRoleMenu(query: object) {
+	return request({
+		url: '/api/system/role_menu_button_permission/get_role_menu/',
+		method: 'get',
+		params: query,
+	}).then((res: any) => {
+		return XEUtils.toArrayTree(res.data, { key: 'id', parentKey: 'parent', children: 'children', strict: false });
+	});
+}
+/**
+ * 设置 角色-菜单
+ * @param data
+ * @returns
+ */
+export function setRoleMenu(data: object) {
+	return request({
+		url: '/api/system/role_menu_button_permission/set_role_menu/',
+		method: 'put',
+		data,
+	});
+}
+/**
+ * 获取 角色-菜单-按钮-列字段
+ * @param query
+ */
+export function getRoleMenuBtnField(query: object) {
+	return request({
+		url: '/api/system/role_menu_button_permission/get_role_menu_btn_field/',
+		method: 'get',
+		params: query,
+	});
+}
+
+/**
+ * 设置 角色-菜单-按钮
+ * @param data
+ */
+export function setRoleMenuBtn(data: object) {
+	return request({
+		url: '/api/system/role_menu_button_permission/set_role_menu_btn/',
+		method: 'put',
+		data,
+	});
+}
+
+/**
+ * 设置 角色-菜单-列字段
+ * @param data
+ */
+export function setRoleMenuField(roleId: string | number | undefined, data: object) {
+	return request({
+		url: `/api/system/role_menu_button_permission/${roleId}/set_role_menu_field/`,
+		method: 'put',
+		data,
+	});
+}
+
+/**
+ * 设置 角色-菜单-按钮-数据权限
+ * @param query
+ * @returns
+ */
+export function setRoleMenuBtnDataRange(data: object) {
+	return request({
+		url: '/api/system/role_menu_button_permission/set_role_menu_btn_data_range/',
+		method: 'put',
+		data,
+	});
+}
+
+/**
+ * 获取当前用户角色下所能授权的部门
+ * @param query
+ * @returns
+ */
+export function getRoleToDeptAll(query: object) {
+	return request({
+		url: '/api/system/role_menu_button_permission/role_to_dept_all/',
+		method: 'get',
+		params: query,
+	});
+}
+
+/**
+ * 获取所有用户
+ * @param query
+ * @returns
+ */
+export function getAllUsers() {
+	return request({
+		url: '/api/system/user/',
+		method: 'get',
+		params: { limit: 999 },
+	}).then((res: any) => {
+		return XEUtils.map(res.data, (item: any) => {
+			return {
+				id: item.id,
+				name: item.name,
+			};
+		});
+	});
+}
+
+/**
+ * 设置角色-用户
+ * @param query
+ * @returns
+ */
+export function setRoleUsers(roleId: string | number | undefined, data: object) {
+	return request({
+		url: `/api/system/role/${roleId}/set_role_users/`,
+		method: 'put',
+		data,
+	});
+}

+ 429 - 0
src/views/questionBank/list/crud.tsx

@@ -0,0 +1,429 @@
+import { CreateCrudOptionsProps, CreateCrudOptionsRet, AddReq, DelReq, EditReq, dict, compute } from '@fast-crud/fast-crud';
+import * as api from './api';
+import { dictionary } from '/@/utils/dictionary';
+import { successMessage } from '../../../utils/message';
+import { auth } from '/@/utils/authFunction';
+
+/**
+ *
+ * @param crudExpose:index传递过来的示例
+ * @param context:index传递过来的自定义参数
+ * @returns
+ */
+export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
+	const pageRequest = async (query: any) => {
+		return await api.GetList(query);
+	};
+	const editRequest = async ({ form, row }: EditReq) => {
+		form.id = row.id;
+		return await api.UpdateObj(form);
+	};
+	const delRequest = async ({ row }: DelReq) => {
+		return await api.DelObj(row.id);
+	};
+	const addRequest = async ({ form }: AddReq) => {
+		return await api.AddObj(form);
+	};
+
+	return {
+		crudOptions: {
+			request: {
+				pageRequest,
+				addRequest,
+				editRequest,
+				delRequest,
+			},
+			pagination: {
+				show: true,
+			},
+			actionbar: {
+				buttons: {
+					add: {
+						show: auth('role:Create'),
+					},
+				},
+			},
+			rowHandle: {
+				//固定右侧
+				fixed: 'right',
+				width: 320,
+				buttons: {
+					view: {
+						show: true,
+					},
+					edit: {
+						show: auth('role:Update'),
+					},
+					remove: {
+						show: auth('role:Delete'),
+					},
+					/* permission: {
+						type: 'primary',
+						text: '权限配置',
+						show: auth('role:Permission'),
+						click: (clickContext: any): void => {
+							const { row } = clickContext;
+							context.RoleDrawer.handleDrawerOpen(row);
+							context.RoleMenuBtn.setState([]);
+							context.RoleMenuField.setState([]);
+						},
+					}, */
+				},
+			},
+			form: {
+				col: { span: 24 },
+				labelWidth: '100px',
+				wrapper: {
+					is: 'el-dialog',
+					width: '600px',
+				},
+			},
+			columns: {
+				/* _index: {
+					title: '序号',
+					form: { show: false },
+					column: {
+						type: 'index',
+						align: 'center',
+						width: '70px',
+						columnSetDisabled: true, //禁止在列设置中选择
+					},
+				}, */
+				id: {
+					title: 'ID',
+					column: { show: false },
+					search: { show: false },
+					form: { show: false },
+				},
+				question: {
+					title: '题目内容',
+					search: { show: true },
+					column: {
+						minWidth: 120,
+						sortable: 'custom',
+					},
+					form: {
+						rules: [{ required: true, message: '题目内容必填' }],
+						component: {
+							placeholder: '请输入题目内容',
+						},
+					},
+				},
+				question_type: {
+					title: '问题类型',
+					search: { show: true },
+					type: 'dict-select',
+					column: {
+						minWidth: 100,
+					},
+					dict: dict({
+						data: [
+							{ value: 1, label: '专业技能' },
+							{ value: 2, label: '通用能力' },
+							{ value: 3, label: '个人特质' }
+						]
+					}),
+					form: {
+						rules: [{ required: true, message: '问题类型必填' }],
+						component: {
+							placeholder: '请选择问题类型',
+						},
+						helper: '选择问题的类型分类',
+					},
+				},
+				question_form: {
+					title: '题目形式',
+					search: { show: true },
+					type: 'dict-select',
+					column: {
+						minWidth: 100,
+					},
+					dict: dict({
+						data: [
+							{ value: 0, label: '开放问题' },
+							{ value: 1, label: '单选题' },
+							{ value: 2, label: '多选题' }
+						]
+					}),
+					form: {
+						rules: [{ required: true, message: '题目形式必填' }],
+						component: {
+							placeholder: '请选择题目形式',
+							onChange: ({ form }) => {
+								// 重置相关字段
+								if (form.question_form === 1) {
+									// 单选题
+									form.options = [{ option_text: '', is_correct: false, sort: 1 }, { option_text: '', is_correct: false, sort: 2 }];
+								} else if (form.question_form === 2) {
+									// 多选题
+									form.options = [{ option_text: '', is_correct: false, sort: 1 }, { option_text: '', is_correct: false, sort: 2 }];
+								} else {
+									// 开放问题
+									form.options = [];
+								}
+							}
+						},
+						helper: '选择题目的形式:开放问题、单选题或多选题',
+					},
+				},
+				position_types: {
+					title: '适用职位',
+					search: { show: true },
+					type: 'dict-select',
+					column: {
+						minWidth: 120,
+					},
+					dict: dict({
+						data: [
+							{ value: '0', label: '技术' },
+									{ value: '1', label: '管理' },
+						]
+					}),
+					form: {
+						component: {
+						/* 	name: 'el-select', */
+							props: {
+								multiple: true,
+								filterable: true,
+								placeholder: '请选择适用职位类型',
+								/* options: [
+									{ value: '0', label: '技术' },
+									{ value: '1', label: '管理' },
+								] */
+							}
+						},
+						helper: '选择题目适用的职位类型,可多选'
+					}
+				},
+				recommended_duration: {
+					title: '建议时长(秒)',
+					search: { show: false },
+					column: {
+						minWidth: 100,
+					},
+					form: {
+						component: {
+							name: 'el-input-number',
+							props: {
+								min: 10,
+								max: 600,
+								step: 10
+							}
+						},
+						helper: '建议回答此题目的时长,单位为秒'
+					}
+				},
+				difficulty: {
+					title: '难度等级',
+					search: { show: true },
+					type: 'dict-select',
+					column: {
+						minWidth: 80,
+					},
+					dict: dict({
+						data: [
+							{ value: 1, label: '初级' },
+							{ value: 2, label: '中级' },
+							{ value: 3, label: '高级' }
+						]
+					}),
+					form: {
+						rules: [{ required: true, message: '难度等级必填' }],
+						helper: '选择题目的难度级别'
+					},
+				},
+				is_system: {
+					title: '系统题目',
+					search: { show: true },
+					column: {
+						width: 80,
+						component: {
+							name: 'el-switch'
+						}
+					},
+					form: {
+						component: {
+							name: 'el-switch',
+							props: {
+								activeText: '是',
+								inactiveText: '否'
+							}
+						},
+						helper: '是否为系统预设题目'
+					}
+				},
+				status: {
+					title: '状态',
+					search: { show: true },
+					type: 'dict-select',
+					column: {
+						width: 80,
+						/* component: {
+							name: 'fs-dict-label',
+							props: {
+								dict: dict({
+									data: [
+										{ value: 0, label: '停用' },
+										{ value: 1, label: '启用' }
+									]
+								})
+							}
+						} */
+					},
+					dict: dict({
+						data: [
+							{ value: 0, label: '停用' },
+							{ value: 1, label: '启用' }
+						]
+					}),
+					form: {
+						rules: [{ required: true, message: '状态必填' }],
+						component: {
+							placeholder: '请选择状态',
+						},
+						helper: '题目的启用状态'
+					},
+				},
+				sort: {
+					title: '排序',
+					search: { show: false },
+					column: {
+						width: 80,
+					},
+					form: {
+						component: {
+							name: 'el-input-number',
+							props: {
+								min: 1,
+								max: 999
+							}
+						},
+						helper: '题目的排序值,值越小排序越靠前'
+					}
+				},
+				option_items: {
+					title: '选项列表',
+					search: { show: false },
+					column: { show: false },
+					form: {
+						show: compute(({ form }) => {
+							return form && (form.question_form === 1 || form.question_form === 2);
+						}),
+						component: {
+							name: 'el-card',
+							children: {
+								default: ({ form }) => {
+									// 确保options数组已初始化
+									if (!form.options) {
+										form.options = [];
+									}
+									
+									return (
+										<div>
+											<div class="option-header" style="display: flex; margin-bottom: 10px; font-weight: bold;">
+												<span style="flex: 1;">选项内容</span>
+												<span style="width: 80px; text-align: center;">是否正确</span>
+											</div>
+											{form.options.map((option, index) => (
+												<div class="option-item" key={index} style="display: flex; align-items: center; margin-bottom: 10px;">
+													<el-input 
+														v-model={option.option_text} 
+														placeholder="请输入选项内容" 
+														style="flex: 1; margin-right: 10px;"
+													/>
+													<el-tooltip 
+														content="设置为正确答案" 
+														placement="top" 
+														effect="light"
+													>
+														<div 
+															onClick={() => {
+																if (form.question_form === 1) {
+																	// 单选题:只能有一个正确答案
+																	form.options.forEach((item, idx) => {
+																		item.is_correct = (idx === index);
+																	});
+																} else {
+																	// 多选题:可以有多个正确答案
+																	option.is_correct = !option.is_correct;
+																}
+															}}
+															style="cursor: pointer; width: 80px; text-align: center;"
+														>
+															{form.question_form === 1 ? (
+																// 单选题使用单选按钮
+																<el-radio 
+																	modelValue={option.is_correct}
+																	label={true}
+																/>
+															) : (
+																// 多选题使用复选框
+																<el-checkbox 
+																	modelValue={option.is_correct}
+																/>
+															)}
+														</div>
+													</el-tooltip>
+												</div>
+											))}
+											<div style="display: flex; justify-content: space-between; margin-top: 10px;">
+												<el-button 
+													type="primary" 
+													onClick={() => {
+														// 确保options数组已初始化
+														if (!form.options) {
+															form.options = [];
+														}
+														// 添加新选项时自动设置排序值
+														const sort = form.options.length > 0 ? 
+															Math.max(...form.options.map(o => o.sort || 0)) + 1 : 1;
+														form.options.push({ option_text: '', is_correct: false, sort });
+													}}
+												>
+													添加选项
+												</el-button>
+												
+												{form.options.length > 2 && (
+													<el-button 
+														type="danger" 
+														onClick={() => {
+															form.options.pop();
+														}}
+													>
+														删除最后一项
+													</el-button>
+												)}
+											</div>
+										</div>
+									)
+								}
+							}
+						},
+						helper: compute(({ form }) => {
+							if (form.question_form === 1) {
+								return '添加单选题的选项,并标记正确答案(只能有一个正确答案)';
+							} else if (form.question_form === 2) {
+								return '添加多选题的选项,并标记正确答案(可以有多个正确答案)';
+							}
+							return '';
+						})
+					}
+				},
+				answer_explanation: {
+					title: '答案解析',
+					search: { show: false },
+					column: { show: false },
+					form: {
+						component: {
+							name: 'el-input',
+							type: 'textarea',
+							rows: 4,
+							placeholder: '请输入答案解析'
+						},
+					},
+				},
+
+			},
+		},
+	};
+};

+ 36 - 0
src/views/questionBank/list/index.vue

@@ -0,0 +1,36 @@
+<template>
+	<fs-page>
+		<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
+		<PermissionDrawerCom />
+	</fs-page>
+</template>
+
+<script lang="ts" setup name="role">
+import { defineAsyncComponent, onMounted } from 'vue';
+import { useFs } from '@fast-crud/fast-crud';
+import { createCrudOptions } from './crud';
+import { RoleDrawerStores } from './stores/RoleDrawerStores';
+import { RoleMenuBtnStores } from './stores/RoleMenuBtnStores';
+import { RoleMenuFieldStores } from './stores/RoleMenuFieldStores';
+import { RoleUsersStores } from './stores/RoleUsersStores';
+
+const PermissionDrawerCom = defineAsyncComponent(() => import('./components/RoleDrawer.vue'));
+
+const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
+const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单
+const RoleMenuField = RoleMenuFieldStores();// 角色-菜单-字段
+const RoleUsers = RoleUsersStores();// 角色-用户
+const { crudBinding, crudRef, crudExpose } = useFs({
+	createCrudOptions,
+	context: { RoleDrawer, RoleMenuBtn, RoleMenuField },
+});
+
+// 页面打开后获取列表数据
+onMounted(async () => {
+	// 刷新
+	crudExpose.doRefresh();
+	// 获取全部用户
+	RoleUsers.get_all_users();
+
+});
+</script>

+ 37 - 0
src/views/questionBank/list/stores/RoleDrawerStores.ts

@@ -0,0 +1,37 @@
+import { defineStore } from 'pinia';
+import { RoleDrawerType } from '../types';
+/**
+ * 权限配置:抽屉
+ */
+const initialState: RoleDrawerType = {
+	drawerVisible: false,
+	roleId: undefined,
+	roleName: undefined,
+	users: [],
+};
+
+export const RoleDrawerStores = defineStore('RoleDrawerStores', {
+	state: (): RoleDrawerType => ({
+		...initialState,
+	}),
+	actions: {
+		/**
+		 * 打开权限修改抽屉
+		 */
+		handleDrawerOpen(row: any) {
+			this.drawerVisible = true;
+			this.set_state(row);
+		},
+		set_state(row: any) {
+			this.roleName = row.name;
+			this.roleId = row.id;
+			this.users = row.users;
+		},
+		/**
+		 * 关闭权限修改抽屉
+		 */
+		handleDrawerClose() {
+			Object.assign(this.$state, initialState);
+		},
+	},
+});

+ 24 - 0
src/views/questionBank/list/stores/RoleMenuBtnStores.ts

@@ -0,0 +1,24 @@
+import { defineStore } from 'pinia';
+import { RoleMenuBtnType } from '../types';
+/**
+ * 权限配置:角色-菜单-按钮
+ */
+
+export const RoleMenuBtnStores = defineStore('RoleMenuBtnStores', {
+	state: (): RoleMenuBtnType[] => [],
+	actions: {
+		/**
+		 * 初始化
+		 */
+		setState(data: RoleMenuBtnType[]) {
+			this.$state = data;
+			this.$state.length = data.length;
+		},
+		updateState(data: RoleMenuBtnType) {
+			const index = this.$state.findIndex((item) => item.id === data.id);
+			if (index !== -1) {
+				this.$state[index] = data;
+			}
+		},
+	},
+});

+ 24 - 0
src/views/questionBank/list/stores/RoleMenuFieldStores.ts

@@ -0,0 +1,24 @@
+import { defineStore } from 'pinia';
+import { RoleMenuFieldType, RoleMenuFieldHeaderType } from '../types';
+/**
+ * 权限配置:角色-菜单-列字段
+ */
+
+export const RoleMenuFieldStores = defineStore('RoleMenuFieldStores', {
+	state: (): RoleMenuFieldType[] => [],
+	actions: {
+		/** 重置 */
+		setState(data: RoleMenuFieldType[]) {
+			this.$state = data;
+			this.$state.length = data.length;
+		},
+	},
+});
+
+export const RoleMenuFieldHeaderStores = defineStore('RoleMenuFieldHeaderStores', {
+	state: (): RoleMenuFieldHeaderType[] => [
+		{ value: 'is_create', label: '新增可见', disabled: 'disabled_create', checked: false },
+		{ value: 'is_update', label: '编辑可见', disabled: 'disabled_update', checked: false },
+		{ value: 'is_query', label: '列表可见', disabled: 'disabled_query', checked: false },
+	],
+});

+ 21 - 0
src/views/questionBank/list/stores/RoleMenuTreeStores.ts

@@ -0,0 +1,21 @@
+import { defineStore } from 'pinia';
+import { RoleMenuTreeType } from '../types';
+/**
+ * 权限抽屉:角色-菜单
+ */
+
+export const RoleMenuTreeStores = defineStore('RoleMenuTreeStores', {
+	state: (): RoleMenuTreeType => ({
+		id: 0,
+		parent: 0,
+		name: '',
+		isCheck: false,
+		is_catalog: false,
+	}),
+	actions: {
+		/** 赋值 */
+		setRoleMenuTree(data: RoleMenuTreeType) {
+			this.$state = data;
+		},
+	},
+});

+ 24 - 0
src/views/questionBank/list/stores/RoleUsersStores.ts

@@ -0,0 +1,24 @@
+import { defineStore } from 'pinia';
+import { RoleUsersType } from '../types';
+import { getAllUsers } from '../components/api';
+import XEUtils from 'xe-utils';
+/**
+ * 权限抽屉:角色-用户
+ */
+
+export const RoleUsersStores = defineStore('RoleUsersStores', {
+	state: (): RoleUsersType => ({
+		all_users: [],
+		right_users: [],
+	}),
+	actions: {
+		get_all_users() {
+			getAllUsers().then((res: any) => {
+				this.$state.all_users = res;
+			});
+		},
+		set_right_users(users: any) {
+			this.$state.right_users = XEUtils.map(users, (item: any) => item.id);
+		},
+	},
+});

+ 99 - 0
src/views/questionBank/list/types.ts

@@ -0,0 +1,99 @@
+/**角色列表数据类型 */
+export interface RoleItemType {
+	id: string | number;
+	modifier_name: string;
+	creator_name: string;
+	create_datetime: string;
+	update_datetime: string;
+	description: string;
+	modifier: string;
+	dept_belong_id: string;
+	name: string;
+	key: string;
+	sort: number;
+	status: boolean;
+	admin: boolean;
+	creator: string;
+}
+
+export interface UsersType {
+	id: string | number;
+	name: string;
+}
+export interface RoleUsersType {
+	all_users: UsersType[];
+	right_users: UsersType[];
+}
+
+/**
+ * 权限配置 抽屉组件参数数据类型
+ */
+export interface RoleDrawerType {
+	/** 是否显示抽屉*/
+	drawerVisible: boolean;
+	/** 角色id*/
+	roleId: string | number | undefined;
+	/** 角色名称*/
+	roleName: string | undefined;
+	/** 用户*/
+	users: UsersType[];
+}
+
+/**
+ * 菜单数据类型
+ */
+export interface RoleMenuTreeType {
+	id: number | string | undefined;
+	/** 父级id */
+	parent: number | string | undefined;
+	name: string;
+	/** 是否选中 */
+	isCheck: boolean;
+	/** 是否是目录 */
+	is_catalog: boolean;
+}
+/**
+ * 菜单-按钮数据类型
+ */
+export interface RoleMenuBtnType {
+	id: string | number;
+	menu_btn_pre_id: string | number;
+	/** 是否选中 */
+	isCheck: boolean;
+	/** 按钮名称 */
+	name: string;
+	/** 数据权限范围 */
+	data_range: number | null;
+	/** 自定义部门 */
+	dept: number[];
+}
+
+/**
+ * 菜单-列字段数据类型
+ */
+export interface RoleMenuFieldType {
+	id: string | number | boolean;
+	/** 模型表字段名 */
+	field_name: string;
+	/** 字段显示名	*/
+	title: string;
+	/** 是否可查询 */
+	is_query: boolean;
+	/** 是否可创建 */
+	is_create: boolean;
+	/** 是否可更新 */
+	is_update: boolean;
+	[key: string]: string | number | boolean;
+}
+/**
+ * 菜单-列字段-标题数据类型
+ */
+export interface RoleMenuFieldHeaderType {
+	value: string;
+	/** 模型表字段名 */
+	label: string;
+	/** 字段显示名	*/
+	disabled: string;
+	/** 是否可查询 */
+	checked: boolean;
+}

+ 80 - 0
src/views/questionBank/positionList/api.ts

@@ -0,0 +1,80 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+import { Session } from '/@/utils/storage';
+
+
+export function GetDocumentList(query: UserPageQuery) {
+  return request({
+    url: '/system/job/questions',
+    method: 'get',
+    params: {...query,tenant_id:'1'},
+  });
+}
+
+export function AddDocument(obj: AddReq) {
+  return request({
+    url: '/system/job/add_question',
+    method: 'post',
+    data: {...obj,position_id:"1"},
+  });
+}
+
+/* 文档管理编辑 /api/system/document/{id}/*/
+export function UpdateDocument(obj: any) {
+	return request({
+		url: '/api/system/document/'+obj.id+ '/',
+		method: 'put',
+		data: obj,
+	});
+}
+
+/* 文档管理查看 /api/system/document/{id}/*/
+export function GetDocument(id: any) {
+	return request({
+		url: '/api/system/document/'+id+ '/',
+		method: 'get',
+	});
+}
+
+/* 文档管理删除 /api/system/document/{id}/*/
+export function DelDocument(id: any) {
+	return request({
+		url: '/api/system/document/'+id+ '/',
+		method: 'delete',
+	});
+}
+
+// 获取文档树结构
+export function GetDocumentTree(query: any) {
+  return request({
+    url: '/api/job/list',
+    method: 'get',
+    params: {...query,tenant_id:'1'},
+  });
+}
+
+// 添加文档分类
+export function AddDocumentCategory(data: any) {
+  return request({
+    url: '/api/system/document-categories/',
+    method: 'post',
+    data:{...data,tenant_id:'1'}
+  });
+}
+
+// 更新文档分类
+export function UpdateDocumentCategory(data: any) {
+  return request({
+    url: `/api/system/document-categories/${data.id}/`,
+    method: 'put',
+    data:{...data,tenant_id:'1'}
+  });
+}
+
+// 删除文档分类
+export function DeleteDocumentCategory(id: any) {
+  return request({
+    url: `/api/system/document-categories/${id}/`,
+    method: 'delete'
+  });
+}

+ 115 - 0
src/views/questionBank/positionList/components/DocumentFormCom/index.vue

@@ -0,0 +1,115 @@
+<template>
+  <div class="document-form-container">
+    <el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
+      <el-form-item label="分类名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入分类名称" />
+      </el-form-item>
+      
+      <el-form-item label="上级分类" prop="parent">
+        <el-tree-select
+          v-model="formData.parent"
+          :data="treeData"
+          :props="{ label: 'name', value: 'id', children: 'children' }"
+          placeholder="请选择上级分类"
+          check-strictly
+          clearable
+        />
+      </el-form-item>
+      
+      <el-form-item label="排序" prop="sort">
+        <el-input-number v-model="formData.sort" :min="0" :max="999" />
+      </el-form-item>
+      
+      <el-form-item label="描述" prop="description">
+        <el-input v-model="formData.description" type="textarea" rows="4" placeholder="请输入分类描述" />
+      </el-form-item>
+      
+      <el-form-item>
+        <el-button type="primary" @click="handleSubmit">保存</el-button>
+        <el-button @click="handleCancel">取消</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, defineProps, defineEmits, onMounted } from 'vue';
+import { AddDocumentCategory, UpdateDocumentCategory } from '../../api';
+import { successNotification } from '/@/utils/message';
+import type { FormInstance } from 'element-plus'
+
+const props = defineProps({
+  initFormData: {
+    type: Object,
+    default: () => ({})
+  },
+  cacheData: {
+    type: Array,
+    default: () => []
+  },
+  treeData: {
+    type: Array,
+    default: () => []
+  }
+});
+
+const emit = defineEmits(['drawerClose']);
+
+const formRef = ref<FormInstance | null>(null);
+const formData = reactive({
+  id: '',
+  name: '',
+  parent: '',
+  sort: 0,
+  description: ''
+});
+
+const rules = {
+  name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }]
+};
+
+// 初始化表单数据
+onMounted(() => {
+  if (props.initFormData && props.initFormData.id) {
+    Object.assign(formData, props.initFormData);
+  }
+});
+
+// 提交表单
+const handleSubmit = async () => {
+  if (!formRef.value) return;
+  
+  await formRef.value.validate(async (valid: any) => {
+    if (valid) {
+      try {
+        let res;
+        if (formData.id) {
+          // 更新
+          res = await UpdateDocumentCategory(formData);
+        } else {
+          // 新增
+          res = await AddDocumentCategory(formData);
+        }
+        
+        if (res && res.code === 2000) {
+          successNotification(res.msg || '操作成功');
+          emit('drawerClose', 'submit');
+        }
+      } catch (error) {
+        console.error('保存文档分类失败:', error);
+      }
+    }
+  });
+};
+
+// 取消
+const handleCancel = () => {
+  emit('drawerClose');
+};
+</script>
+
+<style lang="scss" scoped>
+.document-form-container {
+  padding: 20px;
+}
+</style> 

+ 128 - 0
src/views/questionBank/positionList/components/DocumentTreeCom/index.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="document-tree-container">
+    <div class="document-tree-header">
+      <span class="document-tree-title">职位</span>
+     <!--  <el-button type="primary" size="small" @click="handleAddDocument">
+        <el-icon><Plus /></el-icon>添加分类
+      </el-button> -->
+    </div>
+    <el-tree
+      ref="treeRef"
+      :data="treeData"
+      node-key="id"
+      :props="defaultProps"
+      default-expand-all
+      highlight-current
+      @node-click="handleNodeClick"
+    >
+      <template #default="{ node, data }">
+        <div class="custom-tree-node">
+          <div class="node-label">
+            <el-icon><Folder /></el-icon>
+            <span class="ml-2">{{ data.title }}</span>
+          </div>
+          <div class="node-actions" v-if="data.id">
+            <el-button type="primary" link size="small" @click.stop="handleEdit(data)">
+              <el-icon><Edit /></el-icon>
+            </el-button>
+            <el-button type="danger" link size="small" @click.stop="handleDelete(node, data)">
+              <el-icon><Delete /></el-icon>
+            </el-button>
+          </div>
+        </div>
+      </template>
+    </el-tree>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, defineProps, defineEmits } from 'vue';
+import { Plus, Folder, Edit, Delete } from '@element-plus/icons-vue';
+
+const props = defineProps({
+  treeData: {
+    type: Array,
+    default: () => []
+  }
+});
+
+const emit = defineEmits(['treeClick', 'updateDocument', 'deleteDocument']);
+
+const treeRef = ref(null);
+
+const defaultProps = {
+  children: 'children',
+  label: 'name'
+};
+
+// 处理节点点击
+const handleNodeClick = (data: any) => {
+  emit('treeClick', data);
+};
+
+// 添加文档分类
+const handleAddDocument = () => {
+  emit('updateDocument', 'add');
+};
+
+// 编辑文档分类
+const handleEdit = (data: any) => {
+  emit('updateDocument', 'update', data);
+};
+
+// 删除文档分类
+const handleDelete = (node: any, data: any) => {
+  emit('deleteDocument', data.id, () => {
+    // 回调函数,删除成功后可以执行
+  });
+};
+
+defineExpose({
+  treeRef
+});
+</script>
+
+<style lang="scss" scoped>
+.document-tree-container {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.document-tree-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+}
+
+.document-tree-title {
+  font-size: 16px;
+  font-weight: bold;
+}
+
+.custom-tree-node {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-right: 8px;
+}
+
+.node-label {
+  display: flex;
+  align-items: center;
+}
+
+.node-actions {
+  display: none;
+}
+
+.custom-tree-node:hover .node-actions {
+  display: flex;
+}
+
+.ml-2 {
+  margin-left: 8px;
+}
+</style> 

+ 487 - 0
src/views/questionBank/positionList/crud.tsx

@@ -0,0 +1,487 @@
+import { CreateCrudOptionsProps, CreateCrudOptionsRet, AddReq, DelReq, EditReq, dict, compute } from '@fast-crud/fast-crud';
+import * as api from './api';
+import { dictionary } from '/@/utils/dictionary';
+import { successMessage } from '../../../utils/message';
+import { auth } from '/@/utils/authFunction';
+import { useRouter } from 'vue-router';
+
+export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
+	const router = useRouter(); // 添加这行来获取router实例
+	const pageRequest = async (query: any) => {
+		// 确保查询参数正确传递
+		console.log('Query params:', query); // 添加日志以便调试
+
+		// 构建请求参数,确保category参数被正确传递
+		const params = {
+			...query,
+			tenant_id: '1',
+			// 如果 query.form 中有 category,使用它,否则使用 query.category
+			category: (query.form && query.form.category) || query.category || ''
+		};
+
+		console.log('Request params:', params); // 添加日志以便调试
+
+		return await api.GetDocumentList(params);
+	};
+
+	const editRequest = async ({ form, row }: any) => {
+		return await api.UpdateDocument({
+			...form,
+			id: row.id
+		});
+	};
+
+	const delRequest = async ({ row }: any) => {
+		return await api.DelDocument(row.id);
+	};
+
+	const addRequest = async ({ form }: any) => {
+		return await api.AddDocument(form);
+	};
+
+	return {
+		crudOptions: {
+			request: {
+				pageRequest,
+				addRequest,
+				editRequest,
+				delRequest,
+			},
+			search: {
+				show: true,
+				onSearch: (params: any) => {
+					console.log('Search params:', params);
+					return params;
+				},
+				columns: {
+					category: {
+						title: "分类",
+						search: { show: true, value: '' },
+						type: 'text',
+						column: { show: false } // 表格中不显示此列
+					}
+				}
+			},
+			rowHandle: {
+				width: 250,
+				buttons: {
+					view: {
+						size: 'small',
+						click: ({ row }: any) => {
+							const event = new CustomEvent('viewDocumentDetail', { detail: row });
+							window.dispatchEvent(event);
+						}
+					},
+					preview: {
+						text: '预览',
+						type: 'success',
+						size: 'small',
+						click: (row) => {
+							// 检查文件路径是否存在
+							if (!row.row || !row.row.file_path) {
+								ElMessage.error('文件路径无效');
+								return;
+							}
+
+							const filePath = row.row.file_path;
+							// 获取文件扩展名
+							const fileType = row.row.file_type;
+
+							// 支持的文件类型列表
+							const supportedTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
+
+							if (!supportedTypes.includes(fileType)) {
+								ElMessage.warning(`暂不支持预览该文件类型: ${fileType}`);
+								return;
+							}
+
+							// 使用window.open在新窗口中打开预览页面
+							const previewUrl = `/#/preview?url=${encodeURIComponent(filePath)}&type=${fileType}`;
+							window.open(previewUrl, '_blank');
+						}
+					},
+					edit: {
+						type: 'primary',
+						size: 'small',
+					},
+					remove: {
+						type: 'danger',
+						size: 'small',
+					}
+				}
+			},
+			form: {
+				view: {
+					disabled: true,
+					display: 'text'
+				}
+			},
+			columns: {
+				/* _index: {
+					title: '序号',
+					form: { show: false },
+					column: {
+						type: 'index',
+						align: 'center',
+						width: '70px',
+						columnSetDisabled: true, //禁止在列设置中选择
+					},
+				}, */
+				position_id: {
+					title: '职位',
+					search: { show: true },
+					type: 'dict-select',
+					column: {
+						value: '1',
+						minWidth: 120,
+						show: false,
+					},
+					form: { show: false },
+				},
+				question_id: {
+					title: 'ID',
+					search: { show: false },
+					column: {
+						value: '1',
+						show: false
+					},
+					form: { show: false },
+				},
+				id: {
+					title: 'ID',
+					column: { show: false },
+					search: { show: false },
+					form: { show: false },
+				},
+				question: {
+					title: '题目内容',
+					search: { show: true },
+					column: {
+						minWidth: 120,
+						sortable: 'custom',
+					},
+					form: {
+						rules: [{ required: true, message: '题目内容必填' }],
+						component: {
+							placeholder: '请输入题目内容',
+						},
+					},
+				},
+				question_type: {
+					title: '问题类型',
+					search: { show: true },
+					type: 'dict-select',
+					column: {
+						minWidth: 100,
+					},
+					dict: dict({
+						data: [
+							{ value: 1, label: '专业技能' },
+							{ value: 2, label: '通用能力' },
+							{ value: 3, label: '个人特质' }
+						]
+					}),
+					form: {
+						rules: [{ required: true, message: '问题类型必填' }],
+						component: {
+							placeholder: '请选择问题类型',
+						},
+						helper: '选择问题的类型分类',
+					},
+				},
+				question_form: {
+					title: '题目形式',
+					search: { show: true },
+					type: 'dict-select',
+					column: {
+						minWidth: 100,
+					},
+					dict: dict({
+						data: [
+							{ value: 0, label: '开放问题' },
+							{ value: 1, label: '单选题' },
+							{ value: 2, label: '多选题' }
+						]
+					}),
+					form: {
+						rules: [{ required: true, message: '题目形式必填' }],
+						component: {
+							placeholder: '请选择题目形式',
+							onChange: ({ form }) => {
+								// 重置相关字段
+								if (form.question_form === 1) {
+									// 单选题
+									form.options = [{ option_text: '', is_correct: false, sort: 1 }, { option_text: '', is_correct: false, sort: 2 }];
+								} else if (form.question_form === 2) {
+									// 多选题
+									form.options = [{ option_text: '', is_correct: false, sort: 1 }, { option_text: '', is_correct: false, sort: 2 }];
+								} else {
+									// 开放问题
+									form.options = [];
+								}
+							}
+						},
+						helper: '选择题目的形式:开放问题、单选题或多选题',
+					},
+				},
+				position_types: {
+					title: '适用职位',
+					search: { show: true },
+					type: 'dict-select',
+					column: {
+						minWidth: 120,
+					},
+					dict: dict({
+						data: [
+							{ value: '0', label: '技术' },
+							{ value: '1', label: '管理' },
+						]
+					}),
+					form: {
+						component: {
+							/* 	name: 'el-select', */
+							props: {
+								multiple: true,
+								filterable: true,
+								placeholder: '请选择适用职位类型',
+								/* options: [
+									{ value: '0', label: '技术' },
+									{ value: '1', label: '管理' },
+								] */
+							}
+						},
+						helper: '选择题目适用的职位类型,可多选'
+					}
+				},
+				recommended_duration: {
+					title: '建议时长(秒)',
+					search: { show: false },
+					column: {
+						minWidth: 100,
+					},
+					form: {
+						component: {
+							name: 'el-input-number',
+							props: {
+								min: 10,
+								max: 600,
+								step: 10
+							}
+						},
+						helper: '建议回答此题目的时长,单位为秒'
+					}
+				},
+				difficulty: {
+					title: '难度等级',
+					search: { show: true },
+					type: 'dict-select',
+					column: {
+						minWidth: 80,
+					},
+					dict: dict({
+						data: [
+							{ value: 1, label: '初级' },
+							{ value: 2, label: '中级' },
+							{ value: 3, label: '高级' }
+						]
+					}),
+					form: {
+						rules: [{ required: true, message: '难度等级必填' }],
+						helper: '选择题目的难度级别'
+					},
+				},
+				is_system: {
+					title: '系统题目',
+					search: { show: true },
+					column: {
+						width: 80,
+						component: {
+							name: 'el-switch'
+						}
+					},
+					form: {
+						component: {
+							name: 'el-switch',
+							props: {
+								activeText: '是',
+								inactiveText: '否'
+							}
+						},
+						helper: '是否为系统预设题目'
+					}
+				},
+				status: {
+					title: '状态',
+					search: { show: true },
+					type: 'dict-select',
+					column: {
+						width: 80,
+						/* component: {
+							name: 'fs-dict-label',
+							props: {
+								dict: dict({
+									data: [
+										{ value: 0, label: '停用' },
+										{ value: 1, label: '启用' }
+									]
+								})
+							}
+						} */
+					},
+					dict: dict({
+						data: [
+							{ value: 0, label: '停用' },
+							{ value: 1, label: '启用' }
+						]
+					}),
+					form: {
+						rules: [{ required: true, message: '状态必填' }],
+						component: {
+							placeholder: '请选择状态',
+						},
+						helper: '题目的启用状态'
+					},
+				},
+				sort: {
+					title: '排序',
+					search: { show: false },
+					column: {
+						width: 80,
+					},
+					form: {
+						component: {
+							name: 'el-input-number',
+							props: {
+								min: 1,
+								max: 999
+							}
+						},
+						helper: '题目的排序值,值越小排序越靠前'
+					}
+				},
+				option_items: {
+					title: '选项列表',
+					search: { show: false },
+					column: { show: false },
+					form: {
+						show: compute(({ form }) => {
+							return form && (form.question_form === 1 || form.question_form === 2);
+						}),
+						component: {
+							name: 'el-card',
+							children: {
+								default: ({ form }) => {
+									// 确保options数组已初始化
+									if (!form.options) {
+										form.options = [];
+									}
+
+									return (
+										<div>
+											<div class="option-header" style="display: flex; margin-bottom: 10px; font-weight: bold;">
+												<span style="flex: 1;">选项内容</span>
+												<span style="width: 80px; text-align: center;">是否正确</span>
+											</div>
+											{form.options.map((option, index) => (
+												<div class="option-item" key={index} style="display: flex; align-items: center; margin-bottom: 10px;">
+													<el-input
+														v-model={option.option_text}
+														placeholder="请输入选项内容"
+														style="flex: 1; margin-right: 10px;"
+													/>
+													<el-tooltip
+														content="设置为正确答案"
+														placement="top"
+														effect="light"
+													>
+														<div
+															onClick={() => {
+																if (form.question_form === 1) {
+																	// 单选题:只能有一个正确答案
+																	form.options.forEach((item, idx) => {
+																		item.is_correct = (idx === index);
+																	});
+																} else {
+																	// 多选题:可以有多个正确答案
+																	option.is_correct = !option.is_correct;
+																}
+															}}
+															style="cursor: pointer; width: 80px; text-align: center;"
+														>
+															{form.question_form === 1 ? (
+																// 单选题使用单选按钮
+																<el-radio
+																	modelValue={option.is_correct}
+																	label={true}
+																/>
+															) : (
+																// 多选题使用复选框
+																<el-checkbox
+																	modelValue={option.is_correct}
+																/>
+															)}
+														</div>
+													</el-tooltip>
+												</div>
+											))}
+											<div style="display: flex; justify-content: space-between; margin-top: 10px;">
+												<el-button
+													type="primary"
+													onClick={() => {
+														// 确保options数组已初始化
+														if (!form.options) {
+															form.options = [];
+														}
+														// 添加新选项时自动设置排序值
+														const sort = form.options.length > 0 ?
+															Math.max(...form.options.map(o => o.sort || 0)) + 1 : 1;
+														form.options.push({ option_text: '', is_correct: false, sort });
+													}}
+												>
+													添加选项
+												</el-button>
+
+												{form.options.length > 2 && (
+													<el-button
+														type="danger"
+														onClick={() => {
+															form.options.pop();
+														}}
+													>
+														删除最后一项
+													</el-button>
+												)}
+											</div>
+										</div>
+									)
+								}
+							}
+						},
+						helper: compute(({ form }) => {
+							if (form.question_form === 1) {
+								return '添加单选题的选项,并标记正确答案(只能有一个正确答案)';
+							} else if (form.question_form === 2) {
+								return '添加多选题的选项,并标记正确答案(可以有多个正确答案)';
+							}
+							return '';
+						})
+					}
+				},
+				answer_explanation: {
+					title: '答案解析',
+					search: { show: false },
+					column: { show: false },
+					form: {
+						component: {
+							name: 'el-input',
+							type: 'textarea',
+							rows: 4,
+							placeholder: '请输入答案解析'
+						},
+					},
+				},
+
+			},
+		}
+	};
+}; 

+ 530 - 0
src/views/questionBank/positionList/index.vue

@@ -0,0 +1,530 @@
+<template>
+  <fs-page>
+    <el-row class="document-el-row">
+      <el-col :span="4">
+        <div class="document-box document-left-box">
+          <DocumentTreeCom
+            ref="documentTreeRef"
+            :treeData="documentTreeData"
+            @treeClick="handleTreeClick"
+            @updateDocument="handleUpdateDocument"
+            @deleteDocument="handleDeleteDocument"
+          />
+        </div>
+      </el-col>
+
+      <el-col :span="20">
+        <div class="document-box document-right-box">
+          <fs-crud ref="crudRef" v-bind="crudBinding">
+            <template #form_file="scope">
+              <!-- 只在编辑和添加模式下显示上传功能 -->
+              <template v-if="scope.mode !== 'view'">
+                <el-upload :action="getBaseURL() + 'api/system/minioupload/'" 
+                  
+                  :headers="{
+                    Authorization: 'JWT ' + Session.get('token')
+                  }"
+                  :multiple="false" 
+                  :on-success="(response, file) => handleUploadSuccess(response, file, scope)" 
+                  :drag="false" 
+                  :show-file-list="false">
+                  <el-button type="primary" icon="plus">上传</el-button>
+                </el-upload>
+                <div v-if="uploadedFile" class="uploaded-file-info mt-2">
+                  <el-card shadow="hover" class="w-full">
+                    <div class="flex items-center justify-between">
+                      <div class="flex items-center">
+                        <el-icon class="mr-2"><Document /></el-icon>
+                        <span>{{ uploadedFile.name }}</span>
+                      </div>
+                      <div class="flex items-center">
+                        <span class="text-gray-500 text-sm mr-4">{{ formatFileSize(uploadedFile.file_size) }}</span>
+                        <el-button type="danger" link @click="handleRemoveFile(scope)">
+                          <el-icon><Delete /></el-icon>
+                        </el-button>
+                      </div>
+                    </div>
+                  </el-card>
+                </div>
+              </template>
+              <!-- 查看模式下只显示文件名 -->
+              <template v-else>
+                <div class="flex items-center justify-between">
+                  <span>{{ scope.form.fileName }}</span>
+                  <el-button v-if="isPreviewable(scope.form.file_type)" type="primary" link @click="previewFile(scope.form)">
+                    <el-icon class="mr-1"><View /></el-icon>预览
+                  </el-button>
+                </div>
+              </template>
+            </template>
+          </fs-crud>
+        </div>
+      </el-col>
+    </el-row>
+
+    <el-drawer v-model="drawerVisible" title="文档配置" direction="rtl" size="500px" :close-on-click-modal="false" :before-close="handleDrawerClose">
+      <DocumentFormCom
+        v-if="drawerVisible"
+        :initFormData="drawerFormData"
+        :cacheData="documentTreeCacheData"
+        :treeData="documentTreeData"
+        @drawerClose="handleDrawerClose"
+      />
+    </el-drawer>
+
+    <!-- 添加自定义查看详情对话框 -->
+    <el-dialog
+      v-model="detailDialogVisible"
+      title="文档详情"
+      width="50%"
+      destroy-on-close
+    >
+      <div ref="printArea">
+        <el-descriptions :column="2" size="small" border>
+          <el-descriptions-item label-align="right" label="文档名称">{{ detailData.name || '-' }}</el-descriptions-item>
+          <el-descriptions-item label-align="right" label="文档类型">
+            {{ getDocTypeName(detailData.doc_type) || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label-align="right" label="文件分类">
+            {{ getCategoryName(detailData.category) || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label-align="right" label="所属项目">
+            {{ getProjectName(detailData.project) || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label-align="right" label="文档版本">{{ detailData.version || '-' }}</el-descriptions-item>
+          <el-descriptions-item label-align="right" label="文件类型">{{ detailData.file_type || '-' }}</el-descriptions-item>
+          <el-descriptions-item label-align="right" label="文件大小">{{ formatFileSize(detailData.file_size) || '-' }}</el-descriptions-item>
+          <el-descriptions-item label-align="right" label="状态">
+            <el-tag :type="detailData.status ? 'success' : 'info'">
+              {{ detailData.status ? '启用' : '禁用' }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label-align="right" label="文档描述" :span="2">{{ detailData.doc_desc || '-' }}</el-descriptions-item>
+          <el-descriptions-item label-align="right" label="更新时间">{{ detailData.update_datetime || '-' }}</el-descriptions-item>
+        </el-descriptions>
+      </div>
+      
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button type="primary" @click="handlePrint">
+            <el-icon><Printer /></el-icon>打印
+          </el-button>
+          <el-button @click="detailDialogVisible = false">关闭</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </fs-page>
+</template>
+
+<script lang="ts" setup name="documentList">
+import { ref, onMounted, onBeforeUnmount } from 'vue';
+import { useFs } from '@fast-crud/fast-crud';
+import XEUtils from 'xe-utils';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import { createCrudOptions } from './crud';
+import { Session } from '/@/utils/storage';
+import { getBaseURL } from '/@/utils/baseUrl';
+import { Document, Delete, View, Printer } from '@element-plus/icons-vue';
+import { successNotification } from '/@/utils/message';
+import DocumentTreeCom from './components/DocumentTreeCom/index.vue';
+import DocumentFormCom from './components/DocumentFormCom/index.vue';
+import { GetDocumentTree, DeleteDocumentCategory } from './api';
+import { useRouter } from 'vue-router';
+/* import { print } from '/@/utils/print'; */
+
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
+
+const router = useRouter();
+
+// 存储已上传文件信息
+const uploadedFile = ref(null);
+
+// 树形结构相关数据
+const documentTreeData = ref([]);
+const documentTreeCacheData = ref([]);
+const drawerVisible = ref(false);
+const drawerFormData = ref({});
+const documentTreeRef = ref(null);
+
+// 添加一个响应式变量来存储当前选中的分类ID
+const selectedCategoryId = ref('');
+
+// 添加详情对话框相关数据
+const detailDialogVisible = ref(false);
+const detailData = ref({
+  id: 0,
+  name: '',
+  doc_type: '',
+  category: '',
+  project: '',
+  version: '',
+  file_type: '',
+  file_size: 0,
+  status: true,
+  doc_desc: '',
+  update_datetime: '',
+  file_path: '',
+  file: ''
+});
+
+// 添加打印区域的ref
+const printArea = ref(null);
+
+// 获取文档树数据
+const getTreeData = () => {
+  GetDocumentTree({}).then((ret: any) => {
+    console.log('ret', ret);
+    const responseData = ret.data;
+   /*  const result = XEUtils.toArrayTree(responseData, {
+      parentKey: 'parent',
+      children: 'children',
+      strict: true,
+    }); */
+    documentTreeData.value = responseData;
+  });
+};
+
+// 处理树节点点击
+const handleTreeClick = (record: any) => {
+  console.log('record', record);
+  
+  // 保存当前选中的分类ID
+  selectedCategoryId.value = record?.id || '';
+  
+  // 重置上传文件状态
+  uploadedFile.value = null;
+  
+  // 构建搜索参数
+  const searchParams = {
+    form: {
+      job_id: selectedCategoryId.value
+    }
+  };
+  
+  // 执行搜索
+  crudExpose.doSearch(searchParams);
+};
+
+// 处理文档分类的新增或编辑
+const handleUpdateDocument = (type: any, record: any) => {
+  if (type === 'update' && record) {
+    const parentData = documentTreeRef.value?.treeRef?.currentNode.parent.data || {};
+    documentTreeCacheData.value = [parentData];
+    drawerFormData.value = record;
+  }
+  drawerVisible.value = true;
+};
+
+// 关闭抽屉
+const handleDrawerClose = (type) => {
+  if (type === 'submit') {
+    getTreeData();
+  }
+  drawerVisible.value = false;
+  drawerFormData.value = {};
+};
+
+// 删除文档分类
+const handleDeleteDocument = (id: any, callback: any) => {
+  ElMessageBox.confirm('您确认删除该文档分类吗?', '温馨提示', {
+    confirmButtonText: '确认',
+    cancelButtonText: '取消',
+    type: 'warning',
+  }).then(async () => {
+    const res = await DeleteDocumentCategory(id);
+    callback();
+    if (res?.code === 2000) {
+      successNotification(res.msg);
+      getTreeData();
+    }
+  });
+};
+
+// 处理上传成功
+const handleUploadSuccess = (response: any, uploadFile: any, scope: any) => {
+  if (!response || !response.data) {
+    console.error('Upload response is invalid:', response);
+    return;
+  }
+
+  const fileInfo = {
+    name: uploadFile.name,
+    file_path: response.data.bucket_url,
+    file_type: response.data.file_type,
+    file_size: uploadFile.size,
+    doc_type: scope.form.doc_type || '',
+    category: selectedCategoryId.value, // 使用当前选中的分类ID
+    status: true,
+    doc_desc: '',
+    version: '',
+    /* merchant: Session.get('merchant_info').merchant_id, */
+    file: response.data.bucket_url
+  };
+  
+  // 更新表单数据
+  if (scope.form) {
+    Object.assign(scope.form, fileInfo);
+  } else {
+    console.error('Form reference is not available');
+    return;
+  }
+  
+  // 更新已上传文件显示
+  uploadedFile.value = fileInfo;
+  
+  // 刷新列表时传递category参数
+  crudExpose.doRefresh({
+    form: {
+      category: selectedCategoryId.value
+    }
+  });
+};
+
+// 处理文件删除
+const handleRemoveFile = (scope: any) => {
+  uploadedFile.value = null;
+  // 清空表单数据
+  if (crudRef.value?.form) {
+    const form = crudRef.value.form;
+    form.name = '';
+    form.file_path = '';
+    form.file_type = '';
+    form.file_size = '';
+    form.doc_type = '';
+    form.category = '';
+    form.doc_desc = '';
+    form.version = '';
+    form.file = '';
+    scope.value = '';
+  }
+};
+
+// 格式化文件大小
+const formatFileSize = (bytes: number) => {
+  if (bytes === 0) return '0 B';
+  const k = 1024;
+  const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+  const i = Math.floor(Math.log(bytes) / Math.log(k));
+  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+};
+
+// 判断文件是否可预览
+const isPreviewable = (fileType: string) => {
+  if (!fileType) return false;
+  
+  // 支持的文件类型
+  const supportedTypes = [
+    'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
+    'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'
+  ];
+  
+  return supportedTypes.includes(fileType.toLowerCase());
+};
+
+// 预览文件
+const previewFile = (fileData: any) => {
+  if (!fileData || !fileData.file) {
+    ElMessageBox.alert('文件链接不存在', '预览失败', { type: 'error' });
+    return;
+  }
+  
+  // 获取文件类型
+  const fileType = fileData.file_type || '';
+  
+  const previewUrl = `/#/preview?url=${encodeURIComponent(filePath)}&type=${fileType}`;
+  window.open(previewUrl, '_blank');
+};
+
+// 处理查看详情
+const handleViewDetail = async (event) => {
+  const row = event.detail || event;
+  try {
+    // 可以选择直接使用传入的行数据,或者重新请求完整数据
+    // 如果需要重新请求数据,可以添加一个 GetDocument API 方法
+    // const res = await GetDocument(row.id);
+    // detailData.value = res.data;
+    
+    // 这里直接使用行数据
+    detailData.value = row;
+    detailDialogVisible.value = true;
+  } catch (error) {
+    ElMessage.error('获取文档详情失败');
+  }
+};
+
+// 获取文档类型名称
+const getDocTypeName = (docTypeId) => {
+  if (!docTypeId) return '';
+  const docTypeDict = [
+    { value: 1, label: '合同文档' },
+    { value: 2, label: '技术文档' },
+    { value: 3, label: '操作手册' },
+    { value: 4, label: '维护记录' },
+    { value: 5, label: '其他文档' }
+  ];
+  const docType = docTypeDict.find(item => item.value === docTypeId);
+  return docType ? docType.label : docTypeId;
+};
+
+// 获取分类名称
+const getCategoryName = (categoryId) => {
+  if (!categoryId) return '';
+  const categoryDict = (window as any).__categoryDict || [];
+  const category = categoryDict.find(item => item.id === categoryId);
+  return category ? category.name : categoryId;
+};
+
+// 获取项目名称
+const getProjectName = (projectId) => {
+  if (!projectId) return '';
+  const projectDict = (window as any).__projectDict || [];
+  const project = projectDict.find(item => item.id === projectId);
+  return project ? project.name : projectId;
+};
+
+// 预览详情中的文件
+const previewDetailFile = (fileData) => {
+  if (!fileData || !fileData.file_path) {
+    ElMessage.error('文件路径无效');
+    return;
+  }
+
+  const filePath = fileData.file_path;
+  const fileType = fileData.file_type;
+  
+  // 支持的文件类型列表
+  const supportedTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
+  
+  if (!supportedTypes.includes(fileType)) {
+    ElMessage.warning(`暂不支持预览该文件类型: ${fileType}`);
+    return;
+  }
+
+  // 使用window.open在新窗口中打开预览页面
+  const previewUrl = `/#/preview?url=${encodeURIComponent(filePath)}&type=${fileType}`;
+  window.open(previewUrl, '_blank');
+};
+
+// 处理打印功能
+const handlePrint = () => {
+ /*  print(printArea.value, {
+    title: `${detailData.value.name}文档详情`,
+    style: `
+      .el-descriptions { margin: 10px 0; }
+      .el-descriptions-item__label { font-weight: bold; }
+      .el-tag { padding: 2px 6px; }
+      @media print {
+        .dialog-footer { display: none; }
+      }
+    `
+  }); */
+};
+
+// 页面加载时获取数据
+onMounted(() => {
+  getTreeData();
+  
+  // 先执行一次刷新,确保组件已完全初始化
+  crudExpose.doRefresh();
+  
+  // 监听crud组件初始化完成
+  setTimeout(() => {
+    if (crudRef.value) {
+      // 确保搜索表单已初始化
+      if (!crudRef.value.searchForm) {
+        crudRef.value.searchForm = {};
+      }
+      
+      // 如果有默认选中的分类,设置搜索参数并执行搜索
+      if (selectedCategoryId.value) {
+        crudRef.value.searchForm.category = selectedCategoryId.value;
+        crudExpose.doSearch({
+          form: {
+            category: selectedCategoryId.value
+          }
+        });
+      }
+    }
+  }, 300);
+
+  // 添加全局事件监听器
+  window.addEventListener('viewDocumentDetail', handleViewDetail);
+});
+
+// 添加 onBeforeUnmount 钩子移除事件监听器
+onBeforeUnmount(() => {
+  window.removeEventListener('viewDocumentDetail', handleViewDetail);
+});
+</script>
+
+<style lang="scss" scoped>
+.document-el-row {
+  height: 100%;
+  overflow: hidden;
+
+  .el-col {
+    height: 100%;
+    padding: 10px 0;
+    box-sizing: border-box;
+  }
+}
+
+.document-box {
+  height: 100%;
+  padding: 10px;
+  background-color: var(--el-fill-color-blank);
+  box-sizing: border-box;
+}
+
+.document-left-box {
+  position: relative;
+  border-radius: 0 8px 8px 0;
+  margin-right: 10px;
+}
+
+.document-right-box {
+  border-radius: 8px 0 0 8px;
+}
+
+.mt-2 {
+  margin-top: 0.5rem;
+}
+.w-full {
+  width: 100%;
+}
+.mr-2 {
+  margin-right: 0.5rem;
+}
+.mr-4 {
+  margin-right: 1rem;
+}
+.text-gray-500 {
+  color: #6b7280;
+}
+.text-sm {
+  font-size: 0.875rem;
+}
+.flex {
+  display: flex;
+}
+.items-center {
+  align-items: center;
+}
+.justify-between {
+  justify-content: space-between;
+}
+
+/* 添加详情对话框样式 */
+.el-descriptions {
+  margin: 10px 0;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+}
+
+.mt-4 {
+  margin-top: 1rem;
+}
+</style>

+ 13 - 0
src/views/system/login/component/account.vue

@@ -1,6 +1,16 @@
 <template>
 	<el-form ref="formRef" size="large" class="login-content-form" :model="state.ruleForm" :rules="rules"
 		@keyup.enter="loginClick">
+		<el-form-item class="login-animation1" prop="merchant_code">
+			<!-- <div class="merchant-input-container"> -->
+				<el-input type="text" :placeholder="'请输入商户号'" v-model="ruleForm.merchant_code" clearable autocomplete="off">
+					<template #prefix>
+						<el-icon class="el-input__icon"><ele-Shop /></el-icon>
+					</template>
+				</el-input>
+				<!-- <el-checkbox v-model="rememberMerchant" class="remember-merchant">记住商户号</el-checkbox> -->
+			<!-- </div> -->
+		</el-form-item>
 		<el-form-item class="login-animation1" prop="username">
 			<el-input type="text" :placeholder="$t('message.account.accountPlaceholder1')" v-model="ruleForm.username"
 				clearable autocomplete="off">
@@ -92,12 +102,15 @@ export default defineComponent({
 				captcha: '',
 				captchaKey: '',
 				captchaImgBase: '',
+				merchant_code: localStorage.getItem('merchant_code') || 'ML01',
+				tenant_id:1
 			},
 			loading: {
 				signIn: false,
 			},
 		});
 		const rules = reactive<FormRules>({
+			merchant_code: [{ required: true, message: '请填写商户号', trigger: 'blur' }],
 			username: [
 				{ required: true, message: '请填写账号', trigger: 'blur' },
 			],

+ 2 - 2
src/views/system/login/index.vue

@@ -18,7 +18,7 @@
 				<div class="login-right-warp-mian">
 					<div class="flex flex-col items-center justify-center p-8 pt-14">
 						<!-- Logo -->
-						<div class="text-4xl font-bold text-red-600 mb-12">敏龙AI面试</div>
+						<div class="text-4xl font-bold text-red-600 mb-12">奇才AI面试</div>
 						
 						<!-- 登录表单区域 -->
 						<div class="w-full max-w-sm">
@@ -200,7 +200,7 @@ onMounted(() => {
 			//border: 1px solid var(--el-color-primary-light-3);
 			border-radius: 10px;
 			width: 450px;
-			height: 470px;
+			height: 501px;
 			position: relative;
 			overflow: hidden;
 			//background-color: var(--el-color-white);