Преглед изворни кода

添加平台管理员页面

yangg пре 1 дан
родитељ
комит
1b972757bd

+ 3 - 3
src/stores/userInfo.ts

@@ -71,7 +71,7 @@ export const useUserInfo = defineStore('userInfo', {
 		},
 		async getApiUserInfo() {
 			console.log('Session.get',Session.get('superName'));
-			if(Session.get('superName')=='平台管理员'){
+			/* if(Session.get('superName')=='平台管理员'){
 				return request({
 					url: '/api/platform-admin/admins/1/',
 					method: 'get',
@@ -87,7 +87,7 @@ export const useUserInfo = defineStore('userInfo', {
 					this.userInfos.pwd_change_count = res.data.pwd_change_count;
 					Session.set('userInfo', this.userInfos);
 				})
-			}else{
+			}else{ */
 				return request({
 					url: '/api/system/user/user_info/',
 					method: 'get',
@@ -103,7 +103,7 @@ export const useUserInfo = defineStore('userInfo', {
 					this.userInfos.pwd_change_count = res.data.pwd_change_count;
 					Session.set('userInfo', this.userInfos);
 				})
-			}
+			/* } */
 
 			
 		},

+ 48 - 0
src/views/system/account/api.ts

@@ -0,0 +1,48 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/platform-admin/users/';
+export function GetList(query: UserPageQuery) {
+	return request({
+		url: apiPrefix,
+		method: 'get',
+		params: query,
+	});
+}
+export function GetObj(id: InfoReq) {
+	return request({
+		url: apiPrefix + id,
+		method: 'get',
+	});
+}
+
+export function AddObj(obj: AddReq) {
+	return request({
+		url: apiPrefix,
+		method: 'post',
+		data: obj,
+	});
+}
+
+export function UpdateObj(obj: EditReq) {
+	return request({
+		url: apiPrefix + obj.id + '/',
+		method: 'put',
+		data: obj,
+	});
+}
+
+export function DelObj(id: DelReq) {
+	return request({
+		url: apiPrefix + id + '/',
+		method: 'delete',
+		data: { id },
+	});
+}
+export function GetPermission(query: UserPageQuery) {
+    return request({
+        url: apiPrefix,
+        method: 'get',
+		params: query,
+    });
+} 

+ 217 - 0
src/views/system/account/crud.tsx

@@ -0,0 +1,217 @@
+import * as api from './api';
+import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
+import * as companyApi from '../company/api';
+import { dictionary } from '/@/utils/dictionary';
+import { successMessage } from '/@/utils/message';
+import { auth } from '/@/utils/authFunction';
+import { Md5 } from 'ts-md5';
+
+export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
+	const pageRequest = async (query: any) => {
+		console.log(query)
+		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: {
+			toolbar:{
+				buttons:{
+					search:{show:false},
+					// 刷新按钮
+					refresh:{show:false},
+					// 紧凑模式
+					compact:{show:false},
+					// 导出按钮
+					export:{
+						text: '导出',
+						type: 'primary',
+						size: 'small',
+						icon: 'upload',
+						circle: false,
+						display: true,
+						show:false
+					},
+					// 列设置按钮
+					columns:{
+						show:false
+					},
+				}
+			},
+			search: {
+				col: { span: 3 },
+				show: true,
+				autoSearch:false,
+				buttons: {
+					search: {
+						size: 'small', // 设置查询按钮大小为small
+					},
+					reset: {
+						size: 'small', // 设置重置按钮大小为small
+					}
+				}
+			},
+			request: {
+				pageRequest,
+				addRequest,
+				editRequest,
+				delRequest,
+			},
+			actionbar: {
+				buttons: {
+					add: {
+						show: true,//auth('account:Create'),
+					},
+				},
+			},
+			rowHandle: {
+				fixed: 'right',
+				width: 200,
+				buttons: {
+					view: { iconRight: 'View', type: 'text',show: true },
+					edit: { iconRight: 'Edit', type: 'text', show: false },//auth('account:Update')
+					remove: { iconRight: 'Delete', type: 'text', show: false },//auth('account:Delete')
+				},
+			},
+			columns: {
+				_index: {
+					title: '序号',
+					form: { show: false },
+					column: { type: 'index', align: 'center', width: '70px', columnSetDisabled: true },
+				},
+				username: {
+					title: '用户名',
+					type: 'text',
+					search: { show: true },
+					form: {
+						component: { placeholder: '请输入用户名' },
+						rules: [{ required: true, message: '请输入用户名' }]
+					},
+					column: { minWidth: 120 },
+				},
+				password: {
+					title: '密码',
+					type: 'password',
+					search: { show: false },
+					column: { show: false },
+					form: {
+						component: { placeholder: '请输入密码' },
+						rules: [{ required: true, message: '请输入密码' }],
+						show: true,
+						column: { show: false }
+					},
+					valueResolve({ form }) {
+                        if (form.password) {
+                            form.password = Md5.hashStr(form.password)
+                        }
+                    }
+				},
+				name: {
+					title: '姓名',
+					type: 'text',
+					search: { show: false },
+					form: {
+						component: { placeholder: '请输入姓名' },
+						rules: [{ required: true, message: '请输入姓名' }]
+					},
+					column: { minWidth: 120 },
+				},
+				mobile: {
+					title: '手机号',
+					type: 'text',
+					search: { show: true },
+					form: {
+						component: { placeholder: '请输入手机号' },
+						rules: [{ required: true, message: '请输入手机号' }]
+					},
+					column: { minWidth: 120 },
+				},
+				email: {
+					title: '邮箱',
+					type: 'text',
+					search: { show: true },
+					form: {
+						component: { placeholder: '请输入邮箱' },
+						rules: [{ required: true, message: '请输入邮箱' }]
+					},
+					column: { minWidth: 180 },
+				},
+				tenant_id: {
+					title: '商户ID',
+					search: { show: true },
+					type: 'dict-select',
+					dict: dict({
+						url: '/api/platform/tenants/',
+						value: 'tenant_id',
+						label: 'name',
+						getData: (url) => {
+							return companyApi.GetList({ page: 1, limit: 20 }).then((res:any) => {
+								return res.data.results;
+							});
+						}
+					}),
+				},
+				is_active: {
+					title: '账号状态',
+					search: { show: true },
+					type: 'dict-select',
+					form: { show:false, component: { placeholder: '请选择账号状态' } },
+					column: { minWidth: 120 },
+					dict: dict({
+						data: [
+							{ value: true, label: '启用' },
+							{ value: false, label: '禁用' }
+						]
+					}),
+				},
+				date_joined: {
+					title: '开通时间',
+					type: 'datetime',
+					search: { show: false },
+					form: { component: { placeholder: '请选择开通时间' },show:false },
+					column: { minWidth: 150 },
+				},
+				last_login: {
+					title: '最后登录时间',
+					type: 'datetime',
+					search: { show: false },
+					form: { component: { placeholder: '请选择最后登录时间' },show:false  },
+					column: { minWidth: 150 },
+				},
+				permission_level: {
+					title: '权限等级',
+					search: { show: false },
+					type: 'dict-select',
+					form: { component: { placeholder: '请选择权限等级' },show:false  },
+					column: { minWidth: 120 },
+					dict: dict({
+						data: [
+							{ value: 'admin', label: '管理员' },
+							{ value: 'user', label: '普通用户' },
+							{ value: 'guest', label: '访客' }
+						]
+					}),
+				},
+				/* job_count: {
+					title: '岗位数量',
+					type: 'number',
+					column: { minWidth: 100 },
+				},
+				report_count: {
+					title: '报告数量',
+					type: 'number',
+					column: { minWidth: 100 },
+				}, */
+			},
+		},
+	};
+}; 

+ 150 - 0
src/views/system/account/index.vue

@@ -0,0 +1,150 @@
+<template>
+	<fs-page>
+		<el-row class="account-el-row">
+			<el-col :span="6">
+				<div class="account-box account-left">
+					<el-input v-model="filterVal" :prefix-icon="Search" placeholder="请输入企业名称" />
+					<div class="company-tree-com">
+						<!-- <div class="tc-head">
+							<el-icon size="16" color="#606266" class="tc-head-icon">
+								<HomeFilled />
+							</el-icon>
+							<span class="tc-head-txt">企业架构</span>
+						</div> -->
+						<el-tree
+						style="margin-top: 10px;"
+							ref="treeRef"
+							:data="companyTreeData"
+							:props="defaultTreeProps"
+							:filter-node-method="handleFilterTreeNode"
+							highlight-current
+							@node-click="handleNodeClick"
+						>
+							<template #default="{ node, data }">
+								<span>{{ node.label }}</span>
+							</template>
+						</el-tree>
+					</div>
+				</div>
+			</el-col>
+			<el-col :span="18">
+				<div class="account-box account-table">
+					<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
+				</div>
+			</el-col>
+		</el-row>
+	</fs-page>
+</template>
+
+<script lang="ts" setup name="account">
+import { onMounted, ref, watch } from 'vue';
+import { useFs } from '@fast-crud/fast-crud';
+import { createCrudOptions } from './crud';
+import { GetList } from './api';
+import { handleColumnPermission } from '/@/utils/columnPermission';
+import XEUtils from 'xe-utils';
+import { GetList as companyGetList } from '../company/api';
+import { Search, HomeFilled } from '@element-plus/icons-vue';
+
+const { crudBinding, crudRef, crudExpose, crudOptions, resetCrudOptions } = useFs({ createCrudOptions });
+
+// Company tree related
+interface CompanyTreeItem {
+	id: number;
+	name: string;
+	children?: CompanyTreeItem[];
+}
+const companyTreeData = ref<CompanyTreeItem[]>([]);
+const filterVal = ref('');
+const treeRef = ref();
+const defaultTreeProps = {
+	children: 'children',
+	label: 'name'
+};
+
+const getCompanyData = async () => {
+	const res = await companyGetList({});
+	console.log(res)
+	if (res?.code === 2000 && Array.isArray(res.data.results)) {
+		companyTreeData.value = XEUtils.toArrayTree(res.data.results, {
+			parentKey: 'parent',
+			children: 'children',
+			strict: true
+		});
+	}
+};
+
+const handleFilterTreeNode = (value: string, data: any) => {
+	if (!value) return true;
+	return data.name?.indexOf(value) !== -1;
+};
+
+const handleNodeClick = (data: any) => {
+	console.log(data)
+	crudExpose.doSearch({form: {tenant_id: data.tenant_id}});
+};
+
+watch(filterVal, (val) => {
+	treeRef.value.filter(val);
+});
+
+// Single onMounted
+onMounted(async () => {
+	// Fetch company data
+	await getCompanyData();
+	
+	// Set column permissions
+	const newOptions = await handleColumnPermission(GetList, crudOptions);
+	// Reset crudBinding
+	resetCrudOptions(newOptions);
+	// Refresh
+	crudExpose.doRefresh();
+});
+</script>
+
+<style lang="scss" scoped>
+.account-el-row {
+	height: 100%;
+	overflow: hidden;
+
+	.el-col {
+		height: 100%;
+		padding: 10px 0;
+		box-sizing: border-box;
+	}
+}
+
+.account-box {
+	height: 100%;
+	position: relative;
+	box-sizing: border-box;
+}
+
+.account-left {
+	background-color: var(--el-fill-color-blank);
+	border-radius: 0 8px 8px 0;
+	padding: 10px;
+}
+
+.account-table {
+	margin-left: 10px;
+	padding-bottom: 10px;
+}
+
+.tc-head {
+	display: flex;
+	align-items: center;
+	margin-bottom: 10px;
+}
+
+.tc-head-icon {
+	margin-right: 5px;
+}
+
+.tc-head-txt {
+	flex: 1;
+}
+.company-tree-com{
+	margin-bottom: 20px;
+}
+</style> 

+ 56 - 0
src/views/system/company/api.ts

@@ -0,0 +1,56 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/platform-admin/tenants/';
+export function GetList(query: UserPageQuery) {
+	return request({
+		url: apiPrefix,
+		method: 'get',
+		params: query,
+	});
+}
+export function GetObj(id: InfoReq) {
+	return request({
+		url: apiPrefix + id,
+		method: 'get',
+	});
+}
+
+export function AddObj(obj: AddReq) {
+	return request({
+		url: apiPrefix,
+		method: 'post',
+		data: obj,
+	});
+}
+
+export function UpdateObj(obj: EditReq) {
+	return request({
+		url: apiPrefix + obj.id + '/',
+		method: 'put',
+		data: obj,
+	});
+}
+
+export function DelObj(id: DelReq) {
+	return request({
+		url: apiPrefix + id + '/',
+		method: 'delete',
+		data: { id },
+	});
+}
+export function GetPermission() {
+    return request({
+        url: apiPrefix ,
+        method: 'get',
+    });
+}
+
+// 修改租户状态
+export function updateTenantStatus(id: string, status: number) {
+    return request({
+        url: `${apiPrefix}${id}/activate/`,
+        method: 'post',
+        data: { status },
+    });
+} 

+ 375 - 0
src/views/system/company/crud.tsx

@@ -0,0 +1,375 @@
+import * as api from './api';
+import { updateTenantStatus } from './api';
+import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
+import {GetList} from '../contract/api';
+import { dictionary } from '/@/utils/dictionary';
+import { successMessage } from '/@/utils/message';
+import { auth } from '/@/utils/authFunction';
+import { useRouter } from 'vue-router';
+import { ElMessage, ElMessageBox } from 'element-plus';
+
+export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
+	const router = useRouter();
+	const pageRequest = async (query: UserPageQuery) => {
+		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: {
+			toolbar:{
+				buttons:{
+					search:{show:false},
+					// 刷新按钮
+					refresh:{show:false},
+					// 紧凑模式
+					compact:{show:false},
+					// 导出按钮
+					export:{
+						text: '导出',
+						type: 'primary',
+						size: 'small',
+						icon: 'upload',
+						circle: false,
+						display: true,
+						show:false
+					},
+					// 列设置按钮
+					columns:{
+						show:false
+					},
+				}
+			},
+			request: {
+				pageRequest,
+				addRequest,
+				editRequest,
+				delRequest,
+			},
+			search: {
+				col: { span: 3 },
+				show: true,
+				autoSearch:false,
+				buttons: {
+					search: {
+						size: 'small', // 设置查询按钮大小为small
+					},
+					reset: {
+						size: 'small', // 设置重置按钮大小为small
+					}
+				}
+			},
+			actionbar: {
+				buttons: {
+					add: {
+						size: 'small',
+						show: true//auth('company:Create'),
+					},
+				},
+			},
+			rowHandle: {
+				//固定右侧
+				fixed: 'right',
+				width: 260,
+				buttons: {
+					view: {
+						size: 'small',
+						text: '详细信息',
+						iconRight: 'View',
+						type: 'text',
+						show: true,//auth('company:Detail'),
+					},
+					/* viewContract: {
+						size: 'small',
+						text: '查看合约',
+						iconRight: '',
+						type: 'text',
+						show: true,//auth('company:ViewContract'),
+						click: async ({ row }) => {
+							const res = await GetList({
+								tenant_id: row.id,
+								page: 1,
+								page_size: 10
+							});
+							// TODO: 实现查看合约逻辑,例如导航到合约页面
+							res.data.forEach((item: any) => {
+								console.log(item.tenant,row.tenant_id);
+								if(item.tenant==row.tenant_id){
+									console.log(item);
+									router.push({
+										path: '/system/contract/preview',
+										query: {
+											file: item.document_link,
+											contractName: item.contract_name
+										}
+									});
+								}
+							});
+						}
+					}, */
+					edit: {
+						show: false,
+					},
+					remove: {
+						show: false,
+					},
+					changeStatus: {
+						size: 'small',
+						text: '修改状态',
+						iconRight: 'Edit',
+						type: 'text',
+						show: true,
+						click: async ({ row }) => {
+							const statusMap: Record<string, string> = {
+								'0': '试用',
+								'1': '正式',
+								'2': '暂停',
+								'3': '过期',
+								'4': '禁用'
+							};
+							
+							const { value: status } = await ElMessageBox.prompt('请选择状态:\n0: 试用\n1: 正式\n2: 暂停\n3: 过期\n4: 禁用', '修改状态', {
+								confirmButtonText: '确定',
+								cancelButtonText: '取消',
+								inputPattern: /^[0-4]$/,
+								inputErrorMessage: '请输入有效的状态值(0-4)'
+							});
+							
+							if (status !== undefined) {
+								try {
+									await updateTenantStatus(row.id, parseInt(status));
+									ElMessage.success(`状态已更新为:${statusMap[status]}`);
+									// 刷新表格数据
+									crudExpose.doRefresh();
+								} catch (error) {
+									ElMessage.error('状态更新失败');
+								}
+							}
+						}
+					},
+				},
+			},
+			columns: {
+				_index: {
+					title: '序号',
+					form: { show: false },
+					column: {
+						type: 'index',
+						align: 'center',
+						width: '70px',
+						columnSetDisabled: true, //禁止在列设置中选择
+					},
+				},
+				schema_name: {
+					title: '商户标识',
+					type: 'input',
+					search: { show: true },
+					form: { 
+						rules: [{ required: true, message: '商户标识必填' }],
+						component: { placeholder: '请输入商户标识' },
+					},
+					column: { minWidth: 120 },
+				},
+				name: {
+					title: '商户名称',
+					search: {
+						show: true,
+					},
+					type: 'input',
+					form: {
+						rules: [{ required: true, message: '商户名称必填' }],
+						component: { placeholder: '请输入商户名称' },
+					},
+					column: { minWidth: 120 },
+				},
+				
+				contact_name: {
+					title: '联系人',
+					type: 'input',
+					search: { show: true },
+					form: {
+						rules: [{ required: true, message: '联系人必填' }],
+						component: { placeholder: '请输入联系人姓名' },
+					},
+					column: { minWidth: 100 },
+				},
+				contact_phone: {
+					title: '联系电话',
+					type: 'input',
+					search: { show: true },
+					form: {
+						rules: [
+							{ required: true, message: '联系电话必填' },
+							{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' }
+						],
+						component: { placeholder: '请输入联系电话' },
+					},
+					column: { minWidth: 120 },
+				},
+				contact_email: {
+					title: '联系邮箱',
+					type: 'input',
+					search: { show: true },
+					form: {
+						rules: [
+							{ required: true, message: '联系邮箱必填' },
+							{ type: 'email', message: '请输入正确的邮箱地址' }
+						],
+						component: { placeholder: '请输入联系邮箱' },
+					},
+					column: { minWidth: 150 },
+				},
+				/* domain_name: {
+					title: '域名',
+					type: 'input',
+					search: { show: true },
+					form: {
+						rules: [
+							{ required: true, message: '域名必填' },
+							{ pattern: /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/, message: '请输入正确的域名格式' }
+						],
+						component: { placeholder: '请输入域名' },
+					},
+					column: { minWidth: 180 },
+				}, */
+				admin_user: {
+					title: '管理员信息',
+					type: 'group',
+					form: {
+						group: true,
+					},
+					column: { show: false },
+					children: {
+						'admin_user.username': {
+							title: '管理员账号',
+							type: 'input',
+							form: {
+								rules: [{ required: true, message: '管理员账号必填' }],
+								component: { placeholder: '请输入管理员账号' },
+							},
+						},
+						'admin_user.password': {
+							title: '管理员密码',
+							type: 'password',
+							form: {
+								show:false,
+								rules: [
+									{ required: true, message: '管理员密码必填' },
+									{ min: 6, message: '密码长度不能小于6位' }
+								],
+								component: { 
+									placeholder: '请输入管理员密码',
+									showPassword: true,
+								},
+							},
+						},
+						'admin_user.name': {
+							title: '管理员姓名',
+							type: 'input',
+							form: {
+								rules: [{ required: true, message: '管理员姓名必填' }],
+								component: { placeholder: '请输入管理员姓名' },
+							},
+						},
+						'admin_user.email': {
+							title: '管理员邮箱',
+							type: 'input',
+							form: {
+								rules: [
+									{ required: true, message: '管理员邮箱必填' },
+									{ type: 'email', message: '请输入正确的邮箱地址' }
+								],
+								component: { placeholder: '请输入管理员邮箱' },
+							},
+						},
+					},
+				},
+				/* industry: {
+					title: '行业',
+					type: 'dict-select',
+					search: { show: true },
+					form: { 
+						component: { 
+							placeholder: '请选择行业',
+						} 
+					},
+					column: { minWidth: 100 },
+					dict: dict({
+						data: [
+							{ value: '1', label: '互联网/软件' },
+							{ value: '2', label: '金融/保险' },
+							{ value: '3', label: '制造业' },
+							{ value: '4', label: '教育培训' },
+							{ value: '5', label: '医疗健康' },
+							{ value: '6', label: '房地产/建筑' },
+							{ value: '7', label: '零售/电商' },
+							{ value: '8', label: '餐饮/酒店' },
+							{ value: '9', label: '物流/运输' },
+							{ value: '10', label: '咨询服务' },
+							{ value: '11', label: '媒体/广告' },
+							{ value: '12', label: '能源/化工' },
+							{ value: '13', label: '政府机构' },
+							{ value: '14', label: '非营利组织' },
+							{ value: '15', label: '其他' }
+						]
+					}),
+				}, */
+				status: {
+					title: '状态',
+					search: { show: true },
+					type: 'dict-select',
+					form: { component: { placeholder: '请选择状态' } },
+					column: { minWidth: 120 },
+					dict: dict({
+						data: [
+							{ value: '1', label: '正式' },
+							{ value: '2', label: '暂停' },
+							{ value: '0', label: '试用中' },
+							{value:3,label:'已停用'},
+							{value:4,label:'禁用'}
+						]
+					}),
+				},
+				/* published_jobs_count: {
+					title: '岗位数',
+					type: 'number',
+					column: { minWidth: 90 },
+					
+				}, */
+				/* usageCount: {
+					title: '使用次数',
+					type: 'number',
+					column: { minWidth: 100 },
+				}, */
+				update_datetime: {
+					title: '数据更新时间',
+					type: 'datetime',
+					column: { minWidth: 150 },
+					form:{
+						show:false
+					}
+				},
+				description: {
+					title: '商户描述',
+					type: 'textarea',
+					form: {
+						component: { 
+							placeholder: '请输入商户描述',
+							rows: 3,
+						},
+					},
+					column: { minWidth: 180 },
+				},
+			},
+		},
+	};
+}; 

+ 25 - 0
src/views/system/company/index.vue

@@ -0,0 +1,25 @@
+<template>
+	<fs-page>
+		<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
+	</fs-page>
+</template>
+
+<script lang="ts" setup name="company">
+import { onMounted } from 'vue';
+import { useFs } from '@fast-crud/fast-crud';
+import { createCrudOptions } from './crud';
+import { GetPermission } from './api';
+import { handleColumnPermission } from '/@/utils/columnPermission';
+
+const { crudBinding, crudRef, crudExpose, crudOptions, resetCrudOptions } = useFs({ createCrudOptions });
+
+// 页面打开后获取列表数据
+onMounted(async () => {
+	// 设置列权限
+	const newOptions = await handleColumnPermission(GetPermission, crudOptions);
+	//重置crudBinding
+	resetCrudOptions(newOptions);
+	// 刷新
+	crudExpose.doRefresh();
+});
+</script> 

+ 47 - 0
src/views/system/contract/api.ts

@@ -0,0 +1,47 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/platform-admin/contracts/';
+export function GetList(query: UserPageQuery) {
+	return request({
+		url: apiPrefix,
+		method: 'get',
+		params: query,
+	});
+}
+export function GetObj(id: InfoReq) {
+	return request({
+		url: apiPrefix + id,
+		method: 'get',
+	});
+}
+
+export function AddObj(obj: AddReq) {
+	return request({
+		url: apiPrefix,
+		method: 'post',
+		data: obj,
+	});
+}
+
+export function UpdateObj(obj: EditReq) {
+	return request({
+		url: apiPrefix + obj.id + '/',
+		method: 'put',
+		data: obj,
+	});
+}
+
+export function DelObj(id: DelReq) {
+	return request({
+		url: apiPrefix + id + '/',
+		method: 'delete',
+		data: { id },
+	});
+}
+export function GetPermission() {
+    return request({
+        url: apiPrefix,
+        method: 'get',
+    });
+} 

+ 314 - 0
src/views/system/contract/crud.tsx

@@ -0,0 +1,314 @@
+import * as api from './api';
+import * as companyApi from '../company/api';
+import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
+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();
+	const pageRequest = async (query: UserPageQuery) => {
+		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) => {
+		form.document_link = form.document_link[0];
+		return await api.AddObj(form);
+	};
+
+	const handleViewContract = (row: any) => {
+		console.log(row.document_link);
+		router.push({
+			path: '/system/contract/preview',
+			query: {
+				file: row.document_link,
+				contractName: row.contract_name
+			}
+		});
+	};
+
+	return {
+		crudOptions: {
+			toolbar:{
+				buttons:{
+					search:{show:false},
+					refresh:{show:false},
+					compact:{show:false},
+					export:{
+						text: '导出',
+						type: 'primary',
+						size: 'small',
+						icon: 'upload',
+						circle: false,
+						display: true,
+						show:false
+					},
+					columns:{
+						show:false
+					},
+				}
+			},
+			search: {
+				col: { span: 3 },
+				show: true,
+				autoSearch:false,
+				buttons: {
+					search: {
+						size: 'small',
+					},
+					reset: {
+						size: 'small',
+					}
+				}
+			},
+			request: {
+				pageRequest,
+				addRequest,
+				editRequest,
+				delRequest,
+			},
+			actionbar: {
+				buttons: {
+					add: {
+						show: true,
+					},
+				},
+			},
+			rowHandle: {
+				fixed: 'right',
+				width: 200,
+				buttons: {
+					view: { text: '详细信息', iconRight: 'View', type: 'text', show: true },
+					edit: { iconRight: 'Edit', type: 'text', show: false },
+					remove: { iconRight: 'Delete', type: 'text', show: false },
+					Viewcontract: {
+						text:'查看合约',
+						iconRight:'',
+						type:'text',
+						show:true,
+						click: (opts: any) => {
+							handleViewContract(opts.row);
+						}
+					},
+				},
+			},
+			columns: {
+				_index: {
+					title: '序号',
+					form: { show: false },
+					column: { type: 'index', align: 'center', width: '70px', columnSetDisabled: true },
+				},
+				contract_number: {
+					title: '合约编号',
+					search: { show: true },
+					type: 'input',
+					form: { 
+						rules: [{ required: true, message: '合约编号必填' }],
+						component: { placeholder: '请输入合约编号' }
+					},
+					column: { minWidth: 150 },
+				},
+				document_link: {
+					title: '合约文档',
+					type: 'file-uploader',
+					form: {
+						/* rules: [{ required: true, message: '请上传合约文档' }], */
+						component: {
+							uploader: {
+								type: 'form',
+								action: '/api/uploadfile/',
+								accept: '.pdf,.doc,.docx',
+								maxSize: 10 * 1024 * 1024, // 10MB
+								limit: 1,
+							},
+							placeholder: '请上传PDF或Word格式的合约文档',
+							helpMessage: '支持PDF、Word格式,文件大小不超过10MB'
+						}
+					},
+					column: {
+						show:false,
+						component: {
+							// 自定义显示
+							render: ({ value }) => {
+								if (!value) return '未上传';
+								const fileName = value.split('/').pop();
+								return <el-link type="primary" href={value} target="_blank">{fileName}</el-link>;
+							}
+						},
+						minWidth: 180
+					}
+				},
+				contract_name: {
+					title: '合约名称',
+					search: { show: true },
+					type: 'input',
+					form: { 
+						rules: [{ required: true, message: '合约名称必填' }],
+						component: { placeholder: '请输入合约名称' }
+					},
+					column: { minWidth: 180 },
+				},
+				tenant: {
+					title: '企业名称',
+					search: { show: true },
+					type: 'dict-select',
+					dict: dict({
+						url: '/api/platform/tenants/',
+						value: 'tenant_id',
+						label: 'name',
+						getData: (url) => {
+							return companyApi.GetList({ page: 1, limit: 20 }).then((res:any) => {
+								return res.data.results;
+							});
+						}
+					}),
+					form: { 
+						rules: [{ required: true, message: '企业名称必填' }],
+						component: { 
+							placeholder: '请选择企业名称',
+							filterable: true,
+							clearable: true
+						}
+					},
+					column: { minWidth: 120 },
+				},
+				contract_amount: {
+					title: '合约金额',
+					type: 'number',
+					form: {
+						rules: [{ required: true, message: '合约金额必填' }],
+						component: { 
+							placeholder: '请输入合约金额',
+							precision: 2,
+							step: 1000
+						}
+					},
+					column: { minWidth: 120 },
+				},
+				start_date: {
+					title: '开始时间',
+					type: 'datetime',
+					search: { show: true },
+					form: {
+						rules: [{ required: true, message: '开始时间必填' }],
+						component: { 
+							type: 'datetime',
+							valueFormat: 'YYYY-MM-DD',placeholder: '请选择开始时间' }
+					},
+					column: { minWidth: 150 },
+				},
+				end_date: {
+					title: '结束时间',
+					type: 'datetime',
+					search: { show: true },
+					form: {
+						rules: [{ required: true, message: '结束时间必填' }],
+						component: { type: 'datetime',
+							valueFormat: 'YYYY-MM-DD',placeholder: '请选择结束时间' }
+					},
+					column: { minWidth: 150 },
+				},
+				status: {
+					title: '合约状态',
+					search: { show: true },
+					type: 'dict-select',
+					form: {
+						rules: [{ required: true, message: '合约状态必填' }],
+						component: { placeholder: '请选择合约状态' }
+					},
+					column: { minWidth: 120 },
+					dict: dict({
+						data: [
+							{ value: 1, label: '服务中' },
+							{ value: 2, label: '试用中' },
+							{ value: 0, label: '已停用' }
+						]
+					}),
+				},
+				contact_person: {
+					title: '联系人',
+					type: 'input',
+					form: {
+						rules: [{ required: true, message: '联系人必填' }],
+						component: { placeholder: '请输入联系人姓名' }
+					},
+					column: { minWidth: 120 },
+				},
+				contact_phone: {
+					title: '联系电话',
+					type: 'input',
+					form: {
+						rules: [
+							{ required: true, message: '联系电话必填' },
+							{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' }
+						],
+						component: { placeholder: '请输入联系电话' }
+					},
+					column: { minWidth: 130 },
+				},
+				contact_email: {
+					title: '联系邮箱',
+					type: 'input',
+					form: {
+						rules: [
+							{ required: true, message: '联系邮箱必填' },
+							{ type: 'email', message: '请输入正确的邮箱格式' }
+						],
+						component: { placeholder: '请输入联系邮箱' }
+					},
+					column: { minWidth: 180 },
+				},
+				service_description: {
+					title: '服务描述',
+					type: 'textarea',
+					form: {
+						rules: [{ required: true, message: '服务描述必填' }],
+						component: { 
+							placeholder: '请输入服务描述',
+							showWordLimit: true,
+							maxlength: 500,
+							rows: 3
+						}
+					},
+					column: { minWidth: 200 },
+				},
+				payment_method: {
+					title: '支付方式',
+					type: 'dict-select',
+					form: {
+						rules: [{ required: true, message: '支付方式必填' }],
+						component: { placeholder: '请选择支付方式' }
+					},
+					column: { minWidth: 120 },
+					dict: dict({
+						data: [
+							{ value: 1, label: '一次性付款' },
+							{ value: 2, label: '分期付款' },
+							{ value: 3, label: '其他' }
+						]
+					}),
+				},
+				remarks: {
+					title: '备注',
+					type: 'textarea',
+					form: { 
+						component: { 
+							placeholder: '请输入备注',
+							showWordLimit: true,
+							maxlength: 200,
+							rows: 2
+						}
+					},
+					column: { minWidth: 200 },
+				},
+			},
+		},
+	};
+}; 

+ 92 - 0
src/views/system/contract/index.vue

@@ -0,0 +1,92 @@
+<template>
+	<fs-page>
+		<fs-crud ref="crudRef" v-bind="crudBinding">
+			<!-- <template #form_document_link="{ value, onChange, formData }">
+				<div>
+					<el-upload
+						v-model:file-list="fileList"
+						class="upload-demo"
+						:action="uploadUrl"
+						
+						:on-change="handleChange"
+						:before-upload="beforeUpload"
+						:on-success="handleSuccess"
+						:on-error="handleError"
+						:limit="1"
+					>
+						<el-button type="primary">点击上传</el-button>
+						<template #tip>
+							<div class="el-upload__tip">
+								请上传合同文件,支持 PDF/DOC/DOCX 格式
+							</div>
+						</template>
+					</el-upload>
+				</div>
+			</template> -->
+		</fs-crud>
+	</fs-page>
+</template>
+
+<script lang="ts" setup name="contract">
+import { onMounted, ref } from 'vue';
+import { useFs } from '@fast-crud/fast-crud';
+import { createCrudOptions } from './crud';
+import { GetPermission } from './api';
+import { handleColumnPermission } from '/@/utils/columnPermission';
+import type { UploadFile, UploadFiles } from 'element-plus';
+import { ElMessage } from 'element-plus';
+import { Session } from '/@/utils/storage';
+
+const { crudBinding, crudRef, crudExpose, crudOptions, resetCrudOptions } = useFs({ createCrudOptions });
+
+// 文件列表
+const fileList = ref<UploadFiles>([]);
+
+// 上传地址
+const uploadUrl = ref(import.meta.env.VITE_API_URL + '/api/uploadfile/');
+
+// 上传前验证
+const beforeUpload = (file: File) => {
+  const validTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
+  const isValidType = validTypes.includes(file.type);
+  const isLt10M = file.size / 1024 / 1024 < 10;
+
+  if (!isValidType) {
+    ElMessage.error('只能上传PDF/DOC/DOCX文件!');
+    return false;
+  }
+  if (!isLt10M) {
+    ElMessage.error('文件大小不能超过 10MB!');
+    return false;
+  }
+  return true;
+};
+
+// 文件状态改变时的钩子
+const handleChange = (uploadFile: UploadFile, uploadFiles: UploadFiles) => {
+  console.log('文件变化:', uploadFile, uploadFiles);
+};
+
+// 上传成功回调
+const handleSuccess = (response: any, uploadFile: UploadFile, uploadFiles: UploadFiles) => {
+	
+  ElMessage.success('上传成功');
+  console.log('上传成功:', response);
+};
+
+// 上传失败回调
+const handleError = (error: Error, uploadFile: UploadFile, uploadFiles: UploadFiles) => {
+  ElMessage.error('上传失败');
+  console.error('上传失败:', error);
+};
+
+// 页面打开后获取列表数据
+onMounted(async () => {
+	// 设置列权限
+	const newOptions = await handleColumnPermission(GetPermission, crudOptions);
+	//重置crudBinding
+	resetCrudOptions(newOptions);
+	// 刷新
+	crudExpose.doRefresh();
+});
+</script> 

+ 88 - 0
src/views/system/contract/preview.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="contract-preview">
+    <div class="preview-content">
+      <iframe
+        v-if="previewUrl"
+        :src="previewUrl"
+        class="preview-iframe"
+        frameborder="0"
+        width="100%"
+        height="100%"
+      ></iframe>
+      <el-empty v-else description="文件地址无效">
+        <el-button type="primary" @click="downloadFile">下载文档</el-button>
+      </el-empty>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue';
+import { useRoute } from 'vue-router';
+import { ElMessage } from 'element-plus';
+
+const route = useRoute();
+const loading = ref(true);
+
+const fileUrl = computed(() => route.query.file as string);
+
+// 获取文件扩展名
+const fileExtension = computed(() => {
+  if (!fileUrl.value) return '';
+  return fileUrl.value.split('.').pop()?.toLowerCase() || '';
+});
+
+// 根据文件类型生成预览URL
+const previewUrl = computed(() => {
+  if (!fileUrl.value) return '';
+  
+  // 对URL进行编码
+  const encodedUrl = encodeURIComponent(fileUrl.value);
+  
+  switch (fileExtension.value) {
+    case 'pdf':
+      return fileUrl.value; // PDF直接使用原始URL
+    case 'doc':
+    case 'docx':
+      // 使用Microsoft Office Online Viewer
+      return `https://view.officeapps.live.com/op/embed.aspx?src=${encodedUrl}`;
+      // 备选方案:使用Google Docs Viewer
+      // return `https://docs.google.com/viewer?url=${encodedUrl}&embedded=true`;
+    default:
+      ElMessage.warning('不支持的文件类型,将尝试直接预览');
+      return fileUrl.value;
+  }
+});
+
+const downloadFile = () => {
+  const url = fileUrl.value;
+  if (url) {
+    window.open(url, '_blank');
+  } else {
+    ElMessage.error('文件地址无效');
+  }
+};
+</script>
+
+<style scoped>
+.contract-preview {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.preview-content {
+  flex: 1;
+  background: #fff;
+  border-radius: 4px;
+  box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
+  overflow: hidden;
+  position: relative;
+}
+
+.preview-iframe {
+  width: 100%;
+  height: 100%;
+  border: none;
+}
+</style> 

+ 1 - 1
src/views/system/login/superIndex.vue

@@ -246,7 +246,7 @@ const loginClick = async () => {
 				// 创建一个简单的登录数据对象,避免直接使用响应式对象
 				const loginData = {
 						username: superForm.value.username,
-					password: superForm.value.password,
+						password: superForm.value.password,
 				};
 				/* 	const res =  */
 				loginApi.platformLogin({ ...loginData}).then((res: any) => {