Browse Source

✨ 添加人员管理界面;电脑信息创建修改

xinyan 7 tháng trước cách đây
mục cha
commit
1f8074402c

+ 1 - 1
src/utils/service.ts

@@ -177,7 +177,7 @@ function createRequestFunction(service: any) {
 			baseURL: getBaseURL(),
 			data: {},
 		};
-
+		delete config.headers
 		// const token = userStore.getToken;
 		const token = Session.get('token');
 		if (token != null) {

+ 37 - 0
src/views/computer-information/api.ts

@@ -1,8 +1,10 @@
 import { request } from '/@/utils/service';
+import { Session } from '/@/utils/storage';
 
 
 const apiPrefix = '/api/assets/computer/';
 
+// 卡片展示接口
 export function getCardData(query: any) {
   return request({
     url: apiPrefix + 'card/',
@@ -11,6 +13,7 @@ export function getCardData(query: any) {
   });
 }
 
+// 单条查询接口
 export function getComputerDetailOverview(id: number) {
   return request({
 		url: `${apiPrefix}${id}/`,
@@ -33,11 +36,45 @@ export function getPastTableData(query: any) {
   });
 }
 
+// 下拉框接口
+export function getShopList(query: any) {
+  return request({
+    url: '/api/assets/computer/shop/box/',
+    method: 'GET',
+    params: query,
+  });
+}
+
+export function getUserList(query: any) {
+  return request({
+    url: '/api/assets/people/box/',
+    method: 'GET',
+    params: query,
+  });
+}
+
+// 创建接口
 export function createComputer(body: any) {
   return request({
     url: apiPrefix + 'create/',
     method: 'POST',
     data: body,
+    headers: {
+      'Content-Type': 'multipart/form-data',
+      // 'Authorization': 'JWT ' + Session.get('token')
+    }
   });
 }
 
+// 更新接口
+export function updateComputerDetail(query: any) {
+  return request({
+    url: apiPrefix + `${query.id}/`,
+    method: 'POST',
+    params: { partial: query.partial },
+    data: query.formData,
+  });
+}
+
+
+

+ 74 - 66
src/views/computer-information/components/CreateComputer.vue

@@ -9,20 +9,28 @@ import { useResponse } from '/@/utils/useResponse';
 import * as api from '../api';
 
 const showDialog = defineModel({ default: false });
+const emit = defineEmits([ 'refresh' ]);
+
+const { shopOptions, userOptions } = defineProps<{
+  shopOptions: any;
+  userOptions: any;
+}>();
 
 // 图片处理
 const upload = ref<UploadInstance>();
+
 // 表单数据
 interface RuleForm {
-  computerNumber: string;
-  computerType: string;
-  station: string;
-  shop: string;
-  user: string;
-  macaddress: string;
-  ipaddress: string;
-  images: UploadFile[];
+	computerNumber: string;
+	computerType: string;
+	station: string;
+	shop: string;
+	user: string;
+	macaddress: string;
+	ipaddress: string;
+	images: UploadFile[];
 }
+
 const ruleFormRef = ref<FormInstance>();
 const formData = reactive<RuleForm>({
 	computerNumber: '',
@@ -38,33 +46,14 @@ const formData = reactive<RuleForm>({
 const rules = reactive<FormRules<RuleForm>>({
 	computerNumber: [{ required: true, message: '请输入电脑编号', trigger: 'blur' }],
 	computerType: [{ required: true, message: '请输入电脑类型', trigger: 'blur' }],
-	station: [{ required: true, message: '请输入所在站点', trigger: 'blur' }],
+	station: [{ required: true, message: '请输入工位号', trigger: 'blur' }],
 	shop: [{ required: true, message: '请输入所属商铺', trigger: 'blur' }],
-	user: [{ required: true, message: '请输入使用者', trigger: 'blur' }],
+	user: [{ required: true, message: '请输入电脑使用人', trigger: 'blur' }],
 	macaddress: [{ required: true, message: '请输入MAC地址', trigger: 'blur' }],
 	ipaddress: [{ required: true, message: '请输入IP地址', trigger: 'blur' }],
 });
 
-// 表单字段配置
-const formFields = [
-	{ label: '电脑编号:', key: 'computerNumber'},
-	{ label: '电脑类型:', key: 'computerType'},
-	{ label: '工位号:', key: 'station'},
-	{ label: '所属店铺:', key: 'shop'},
-	{ label: '电脑使用人:', key: 'user'},
-	{ label: 'MAC地址:', key: 'macaddress'},
-	{ label: '使用IP地址:', key: 'ipaddress'},
-];
-const shopOptions = [
-	{
-		value: 'shop1',
-		label: '店铺一',
-	},
-	{
-		value: 'shop2',
-		label: '店铺二',
-	},
-];
+
 
 // 超过图片限制时触发的回调
 const handleExceed: UploadProps['onExceed'] = (files) => {
@@ -75,55 +64,74 @@ const handleExceed: UploadProps['onExceed'] = (files) => {
 };
 
 const submitForm = async (formEl: FormInstance | undefined) => {
-  if (!formEl) return
-  await formEl.validate((valid, fields) => {
-    if (valid) {
-      handleSave();
-      console.log('submit!')
-    } else {
-      console.log('error submit!', fields)
-    }
-  })
-}
+	if (!formEl) return;
+	await formEl.validate((valid, fields) => {
+		if (valid) {
+			handleSave();
+			showDialog.value = false;
+      emit('refresh');
+		} else {
+			ElMessage.error('添加失败');
+		}
+	});
+};
 
 // 保存操作
-async function handleSave () {
-	const body = {
-		computerNumber: formData.computerNumber,
-		computerType: formData.computerType,
-		station: formData.station,
-		shop: formData.shop,
-		user: formData.user,
-		macaddress: formData.macaddress,
-		ipaddress: formData.ipaddress,
-		images: formData.images,
-	};
-  await useResponse(body, api.createComputer);
-  console.log("=>(CreateComputer.vue:94) body", body);
+async function handleSave() {
+	const bodyData = new FormData();
+	const fieldsToAppend = ['computerNumber', 'computerType', 'station', 'user', 'macaddress', 'ipaddress'];
+
+	fieldsToAppend.forEach((field) => {
+		bodyData.append(field, formData[field as keyof RuleForm]);
+	});
+
+	const shopValue = Array.isArray(formData.shop) ? formData.shop.join(',') : formData.shop;
+	bodyData.append('shops', shopValue);
+
+	if (formData.images.length > 0 && formData.images[0].raw) {
+		bodyData.append('images', formData.images[0].raw);
+	}
+	const resp = await useResponse(bodyData, api.createComputer);
+	if (resp.code === 2000) {
+		ElMessage.success('添加成功');
+	}
 }
 
 // 取消操作
 const handleCancel = () => {
-	router.push({
-		path: '/computer',
-	});
+	showDialog.value = false;
 };
+
+onMounted(() => {});
 </script>
 
 <template>
 	<el-dialog v-model="showDialog" :close-on-click-modal="false" title="新增电脑信息" width="35%">
-		<el-form
-        ref="ruleFormRef"
-        :model="formData"
-        :rules="rules"
-        class="computer-info-form"
-        label-width="auto"
-        status-icon>
-			<el-form-item v-for="field in formFields" :label="field.label" :prop="field.key">
-				<el-select v-if="field.key === 'shop'" v-model="formData[field.key]" placeholder="请选择店铺">
+		<el-form ref="ruleFormRef" :model="formData" :rules="rules" class="computer-info-form" label-width="auto" status-icon>
+			<el-form-item label="电脑编号:" prop="computerNumber">
+				<el-input v-model="formData.computerNumber" placeholder="请输入电脑编号" />
+			</el-form-item>
+			<el-form-item label="电脑类型:" prop="computerType">
+				<el-input v-model="formData.computerType" placeholder="请输入电脑类型" />
+			</el-form-item>
+			<el-form-item label="工位号:" prop="station">
+				<el-input v-model="formData.station" placeholder="请输入工位号" />
+			</el-form-item>
+			<el-form-item label="所属店铺:" prop="shop">
+				<el-select v-model="formData.shop" clearable collapse-tags filterable multiple placeholder="请选择店铺">
 					<el-option v-for="shop in shopOptions" :key="shop.value" :label="shop.label" :value="shop.value" />
 				</el-select>
-				<el-input v-else v-model="formData[field.key]" />
+			</el-form-item>
+			<el-form-item label="电脑使用人:" prop="user">
+				<el-select v-model="formData.user" placeholder="请选择电脑使用人">
+					<el-option v-for="user in userOptions" :key="user.value" :label="user.label" :value="user.value" />
+				</el-select>
+			</el-form-item>
+			<el-form-item label="MAC地址:" prop="macaddress">
+				<el-input v-model="formData.macaddress" placeholder="请输入MAC地址" />
+			</el-form-item>
+			<el-form-item label="使用IP地址:" prop="ipaddress">
+				<el-input v-model="formData.ipaddress" placeholder="请输入IP地址" />
 			</el-form-item>
 			<!-- 电脑图片上传 -->
 			<el-form-item :key="images" label="电脑图片:">

+ 86 - 61
src/views/computer-information/components/EditComputerInfo.vue

@@ -5,14 +5,24 @@
  * @Author: xinyan
  */
 import { ref } from 'vue';
-import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
+import { FormInstance, FormRules, UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
 import { genFileId } from 'element-plus';
 import router from '/@/router';
+import { cloneDeep } from 'lodash';
+
+const visible = defineModel({ default: false });
+const { computerInfo, computerNumber , userOptions, shopOptions } = defineProps<{
+  computerInfo: any;
+  computerNumber: any;
+  userOptions: any;
+  shopOptions: any;
+}>();
 
 // 图片处理
 const upload = ref<UploadInstance>();
 // 表单数据
-const formData = ref({
+const ruleFormRef = ref<FormInstance>();
+const formData = reactive<RuleForm>({
 	computerNumber: '',
 	computerType: '',
 	station: '',
@@ -22,28 +32,19 @@ const formData = ref({
 	ipaddress: '',
 	images: [],
 });
+const rules = reactive<FormRules<RuleForm>>({
+  computerNumber: [{ required: true, message: '请输入电脑编号', trigger: 'blur' }],
+  computerType: [{ required: true, message: '请输入电脑类型', trigger: 'blur' }],
+  station: [{ required: true, message: '请输入所在站点', trigger: 'blur' }],
+  shop: [{ required: true, message: '请输入所属商铺', trigger: 'blur' }],
+  user: [{ required: true, message: '请输入使用者', trigger: 'blur' }],
+  macaddress: [{ required: true, message: '请输入MAC地址', trigger: 'blur' }],
+  ipaddress: [{ required: true, message: '请输入IP地址', trigger: 'blur' }],
+});
 
-const shopOptions = [
-	{
-		value: 'shop1',
-		label: '店铺一',
-	},
-	{
-		value: 'shop2',
-		label: '店铺二',
-	},
-];
-
-// 表单字段配置
-const formFields = [
-	{ label: '电脑编号:', key: 'computerNumber', required: true },
-	{ label: '电脑类型:', key: 'computerType', required: true },
-	{ label: '工位号:', key: 'station', required: true },
-	{ label: '所属店铺:', key: 'shop', required: true },
-	{ label: '电脑使用人:', key: 'user', required: true },
-	{ label: 'MAC地址:', key: 'macaddress', required: true },
-	{ label: '使用IP地址:', key: 'ipaddress', required: true },
-];
+onBeforeMount(() => {
+  replaceCol();
+});
 
 // 超过图片限制时触发的回调
 const handleExceed: UploadProps['onExceed'] = (files) => {
@@ -64,47 +65,71 @@ const handleCancel = () => {
 		path: '/computer',
 	});
 };
+
+function replaceCol() {
+  const result = Object.keys(formData).reduce((acc, key) => {
+    if (key in computerInfo) {
+      acc[key] = computerInfo[key];
+    }
+    return acc;
+  }, {} as { [key: string]: any });
+  Object.assign(formData, result);
+}
 </script>
 
 <template>
-	<el-card class="m-3.5">
-		<div class="font-bold text-xl pb-3.5">电脑信息编辑</div>
-		<el-form :model="formData" class="computer-info-form" label-width="120px">
-			<!-- 使用 v-for 循环渲染表单项 -->
-			<el-form-item v-for="(field, index) in formFields" :key="index" :label="field.label" :required="field.required" size="large">
-				<!-- 判断所属店铺,渲染 el-select -->
-				<el-select v-if="field.key === 'shop'" v-model="formData[field.key]" placeholder="请选择店铺" style="width: 70%">
-					<el-option v-for="shop in shopOptions" :key="shop.value" :label="shop.label" :value="shop.value" />
-				</el-select>
-
-				<!-- 其他字段渲染 el-input -->
-				<el-input v-else v-model="formData[field.key]" style="width: 70%" />
-			</el-form-item>
+	<el-drawer :title="`电脑编辑 - ${computerNumber}`" v-model="visible" size="30%">
+			<el-form  ref="ruleFormRef" :model="formData" :rules="rules" class="computer-info-form" label-width="auto">
+        <el-form-item label="电脑编号:" prop="computerNumber">
+          <el-input v-model="formData.computerNumber" placeholder="请输入电脑编号" />
+        </el-form-item>
+        <el-form-item label="电脑类型:" prop="computerType">
+          <el-input v-model="formData.computerType" placeholder="请输入电脑类型" />
+        </el-form-item>
+        <el-form-item label="工位号:" prop="station">
+          <el-input v-model="formData.station" placeholder="请输入工位号" />
+        </el-form-item>
+        <el-form-item label="所属店铺:" prop="shop">
+          <el-select v-model="formData.shop" clearable filterable multiple collapse-tags placeholder="请选择店铺">
+            <el-option v-for="shop in shopOptions" :key="shop.value" :label="shop.label" :value="shop.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="电脑使用人:" prop="user">
+          <el-select v-model="formData.user" placeholder="请选择电脑使用人">
+            <el-option v-for="user in userOptions" :key="user.value" :label="user.label" :value="user.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="MAC地址:" prop="macaddress">
+          <el-input v-model="formData.macaddress" placeholder="请输入MAC地址" />
+        </el-form-item>
+        <el-form-item label="使用IP地址:" prop="ipaddress">
+          <el-input v-model="formData.ipaddress" placeholder="请输入IP地址" />
+        </el-form-item>
 
-			<!-- 电脑图片上传 -->
-			<el-form-item :key="images" label="电脑图片:">
-				<el-upload
-					ref="upload"
-					v-model:file-list="formData.images"
-					:auto-upload="false"
-					:limit="1"
-					:on-exceed="handleExceed"
-					action="#"
-					list-type="picture-card"
-				>
-					<el-icon>
-						<Plus />
-					</el-icon>
-				</el-upload>
-			</el-form-item>
+				<!-- 电脑图片上传 -->
+				<el-form-item :key="images" label="电脑图片:">
+					<el-upload
+						ref="upload"
+						v-model:file-list="formData.images"
+						:auto-upload="false"
+						:limit="1"
+						:on-exceed="handleExceed"
+						action="#"
+						list-type="picture-card"
+					>
+						<el-icon>
+							<Plus />
+						</el-icon>
+					</el-upload>
+				</el-form-item>
 
-			<!-- 按钮组 -->
-			<el-form-item class="button-group">
-				<el-button type="primary" @click="handleSave">保存</el-button>
-				<el-button @click="handleCancel">取消</el-button>
-			</el-form-item>
-		</el-form>
-	</el-card>
+				<!-- 按钮组 -->
+				<el-form-item class="button-group">
+					<el-button type="primary" @click="handleSave">保存</el-button>
+					<el-button @click="handleCancel">取消</el-button>
+				</el-form-item>
+			</el-form>
+	</el-drawer>
 </template>
 
 <style scoped>
@@ -118,7 +143,7 @@ const handleCancel = () => {
 }
 
 .computer-info-form :deep(.el-form-item__label) {
-	font-size: 15px;
-	font-weight: bold;
+	font-size: 14px;
+	font-weight: 500;
 }
 </style>

+ 76 - 26
src/views/computer-information/components/InfoCard.vue

@@ -6,23 +6,40 @@
  */
 
 import { ref } from 'vue';
-import { ElIcon, ElMessage } from 'element-plus';
-import { Delete, EditPen, Picture as IconPicture, Search } from '@element-plus/icons-vue';
+import { ElCol, ElIcon } from 'element-plus';
+import { EditPen, Picture as IconPicture, Plus, Search } from '@element-plus/icons-vue';
 import * as api from '/@/views/computer-information/api';
 import { useRouter } from 'vue-router';
 import { usePagination } from '/@/utils/usePagination';
 import { useTableData } from '/@/utils/useTableData';
+import EditComputerInfo from '/@/views/computer-information/components/EditComputerInfo.vue';
+import { useResponse } from '/@/utils/useResponse';
+import CreateComputer from '/@/views/computer-information/components/CreateComputer.vue';
 
 const router = useRouter();
 const loading = ref();
+
+const showDialog = ref(false);
+const isDrawerVisible = ref(false);
+
+const computerInfo = ref([]);
+const shopOptions = ref([]);
+const userOptions = ref([]);
+
 const { tableOptions, handlePageChange } = usePagination(fetchCardData);
+tableOptions.value.limit = 12;
 
 async function fetchCardData() {
 	const query = {
 		page: tableOptions.value.page,
 		limit: tableOptions.value.limit,
 	};
-	await useTableData(api.getCardData, query, tableOptions);
+	await useTableData(api.getCardData, query, tableOptions,loading);
+}
+
+// 打开创建弹窗
+async function addComputer() {
+  showDialog.value = true;
 }
 
 const checkItem = (item) => {
@@ -35,20 +52,13 @@ const checkItem = (item) => {
 	});
 };
 
-// 编辑和删除的事件处理
-const editItem = (item) => {
-	router.push({
-		path: '/computer/edit',
-		query: { computerNumber: item.computerNumber },
-	});
-};
-
-const deleteItem = (item) => {
-	ElMessage({
-		message: `删除 ${item.computerNumber}`,
-		type: 'warning',
-	});
-};
+async function editItem(item) {
+	const res = await useResponse(item.id, api.getComputerDetailOverview);
+	computerInfo.value = res.data;
+	if (computerInfo.value) {
+		isDrawerVisible.value = true; // 显示 Drawer
+	}
+}
 
 // 处理图片地址
 const getImageUrl = (images) => {
@@ -56,17 +66,53 @@ const getImageUrl = (images) => {
 	return images.length > 0 ? images[0].image_url : '';
 };
 
+async function fetchShopOptions() {
+  try {
+    const resp = await useResponse(null, api.getShopList);
+    shopOptions.value = resp.data.map((item: any) => {
+      return { value: item.id, label: item.platformNumber };
+    });
+  } catch (e) {
+    console.log('error', e);
+  }
+}
+
+async function fetchUserOptions() {
+  try {
+    const resp = await useResponse(null, api.getUserList);
+    userOptions.value = resp.data.map((item: any) => {
+      return { value: item.id, label: item.name };
+    });
+  } catch (e) {
+    console.log('error', e);
+  }
+}
+
 onMounted(() => {
 	fetchCardData();
+  fetchShopOptions();
+  fetchUserOptions();
 });
 </script>
 
 <template>
+  <!-- 标题区域 -->
+  <el-card class="my-2.5" shadow="never" style="border: none;">
+    <div class="flex justify-between items-baseline">
+      <div>
+        <span class="font-bold text-xl">电脑信息概览</span>
+        <el-divider class=" text-3xl" direction="vertical"/>
+      </div>
+      <span>
+           <el-button :icon="Plus" text bg type="primary" @click="addComputer">添 加</el-button>
+        </span>
+    </div>
+  </el-card>
 	<!-- 卡片展示区域 -->
-	<el-card shadow="never" style="border: none; min-height: 750px; position: relative;">
+	<el-card v-loading="loading" shadow="never" style="border: none; min-height: 820px; position: relative">
 		<div class="card-container">
 			<el-row :gutter="20">
-				<el-col v-for="(item, index) in tableOptions.data" :key="index" :span="4" class="my-2.5">
+				<el-col v-for="(item, index) in tableOptions.data" :key="index" :lg="6" :md="8" :sm="8" :xl="4" :xs="12" class="my-2.5">
 					<el-card class="item-card" shadow="hover">
 						<el-image :src="getImageUrl(item.images)" alt="电脑图片" class="card-image">
 							<template #error>
@@ -77,28 +123,28 @@ onMounted(() => {
 						</el-image>
 						<div class="card-content">
 							<div>
-								<span style="color: #808d97;font-weight: 500">电脑编号: </span>
+								<span style="color: #808d97; font-weight: 500">电脑编号: </span>
 								<span style="font-weight: 500">{{ item.computerNumber }}</span>
 							</div>
 							<div>
-								<span style="color: #808d97;font-weight: 500">所属店铺: </span>
+								<span style="color: #808d97; font-weight: 500">所属店铺: </span>
 								<span style="font-weight: 500">{{ item.shopName }}</span>
 							</div>
 						</div>
 						<div class="card-footer">
-							<el-button :icon="Search" circle text bg type="primary" @click="checkItem(item)" />
-							<el-button :icon="EditPen" circle text bg type="warning" @click="editItem(item)" />
-							<el-button :icon="Delete" circle text bg type="danger" @click="deleteItem(item)" />
+							<el-button :icon="Search" bg circle text type="primary" @click="checkItem(item)" />
+							<el-button :icon="EditPen" bg circle text type="warning" @click="editItem(item)" />
+							<!--<el-button :icon="Delete" circle text bg type="danger" @click="deleteItem(item)" />-->
 						</div>
 					</el-card>
 				</el-col>
 			</el-row>
 		</div>
-		<div class="pagination-container" style="position: absolute; right: 32px; bottom: 32px;">
+		<div class="pagination-container" style="position: absolute; right: 32px; bottom: 32px">
 			<el-pagination
 				v-model:current-page="tableOptions.page"
 				v-model:page-size="tableOptions.limit"
-				:page-sizes="[6, 12, 24,36,48,60]"
+				:page-sizes="[6, 12, 24, 36, 48, 60]"
 				:total="tableOptions.total"
 				background
 				layout="sizes, prev, pager, next, total"
@@ -106,6 +152,10 @@ onMounted(() => {
 			/>
 		</div>
 	</el-card>
+  <!-- 新增 Dialog -->
+  <CreateComputer v-model="showDialog" :shopOptions :userOptions @refresh="fetchCardData"></CreateComputer>
+  <!-- 编辑 Drawer -->
+  <EditComputerInfo v-if="isDrawerVisible" v-model="isDrawerVisible" :shopOptions="shopOptions" :userOptions="userOptions" :computerInfo :computerNumber="computerInfo.computerNumber" />
 </template>
 
 <style lang="scss" scoped>

+ 4 - 23
src/views/computer-information/index.vue

@@ -6,34 +6,15 @@
  */
 
 import InfoCard from '/@/views/computer-information/components/InfoCard.vue';
-import { Plus } from '@element-plus/icons-vue';
-import CreateComputer from '/@/views/computer-information/components/CreateComputer.vue';
-
-// 创建弹窗
-const showDialog = ref(false);
-
-// 打开创建弹窗
-async function addComputer() {
-  showDialog.value = true;
-}
+import { useResponse } from '/@/utils/useResponse';
+import * as api from '/@/views/computer-information/api';
+import { ref } from 'vue';
 
 </script>
 
 <template>
 	<div class="px-2.5">
-    <el-card class="my-2.5" shadow="never" style="border: none;">
-      <div class="flex justify-between items-baseline">
-        <div>
-          <span class="font-bold text-xl">电脑信息概览</span>
-          <el-divider class=" text-3xl" direction="vertical"/>
-        </div>
-        <span>
-           <el-button :icon="Plus" plain type="primary" @click="addComputer">添 加</el-button>
-        </span>
-      </div>
-    </el-card>
-			<InfoCard />
-    <CreateComputer v-model="showDialog"></CreateComputer>
+		<InfoCard />
 	</div>
 </template>
 

+ 55 - 0
src/views/employee-information/api.ts

@@ -0,0 +1,55 @@
+import { request } from '/@/utils/service';
+
+const apiPrefix = '/api/assets/people/';
+
+// 卡片展示接口
+export function getCardData(query: any) {
+	return request({
+		url: apiPrefix + 'card/',
+		method: 'GET',
+		params: query,
+	});
+}
+
+// 单条查询接口
+export function getEmployeeDetailOverview(id: number) {
+	return request({
+		url: `${apiPrefix}${id}/`,
+		method: 'GET',
+	});
+}
+
+export function getComputerTableData(query: any) {
+	return request({
+		url: apiPrefix + 'computer/',
+		method: 'GET',
+		params: query,
+	});
+}
+
+export function getShopTableData(query: any) {
+	return request({
+		url: apiPrefix + 'shop/',
+		method: 'GET',
+		params: query,
+	});
+}
+
+// 创建接口
+export function createEmployee(body: any) {
+	return request({
+		url: apiPrefix + 'create/',
+		method: 'POST',
+		data: body,
+	});
+}
+
+// 更新接口
+export function updateEmployeeDetail(query: any) {
+	return request({
+		url: apiPrefix + `${query.id}/`,
+		method: 'POST',
+		params: { partial: query.partial },
+		data: query.formData,
+	});
+}

+ 138 - 0
src/views/employee-information/components/CreateEmployee.vue

@@ -0,0 +1,138 @@
+<script lang="ts" setup>
+/**
+ * @Name: CreateEmployee.vue
+ * @Description:
+ * @Author: xinyan
+ */
+import { ElMessage, FormInstance, FormRules } from 'element-plus';
+import { useResponse } from '/@/utils/useResponse';
+import * as api from '../api';
+
+const showDialog = defineModel({ default: false });
+
+// 图片处理
+const upload = ref<UploadInstance>();
+// 表单数据
+interface RuleForm {
+  name: string;
+  department: string;
+  phone: string;
+  email: string;
+  user: string;
+  macaddress: string;
+  ipaddress: string;
+  images: UploadFile[];
+}
+const ruleFormRef = ref<FormInstance>();
+const formData = reactive<RuleForm>({
+  name: '',
+  department: '',
+  phone: '',
+  email: '',
+  images: [],
+});
+
+const rules = reactive<FormRules<RuleForm>>({
+  name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+  department: [{ required: true, message: '请输入部门', trigger: 'blur' }],
+  phone: [{ required: true, message: '请输入电话号码', trigger: 'blur' }],
+  email: [{ required: true, message: '请输入所属商铺', trigger: 'blur' }],
+});
+
+// 超过图片限制时触发的回调
+const handleExceed: UploadProps['onExceed'] = (files) => {
+  upload.value!.clearFiles();
+  const file = files[0] as UploadRawFile;
+  file.uid = genFileId();
+  upload.value!.handleStart(file);
+};
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      handleSave();
+      ElMessage.success('添加成功');
+    } else {
+      ElMessage.error('添加失败');
+    }
+  })
+}
+// 保存操作
+async function handleSave() {
+  const bodyData = new FormData();
+  const fieldsToAppend = ['name', 'department', 'phone', 'email'];
+  fieldsToAppend.forEach((field) => {
+    bodyData.append(field, formData[field as keyof RuleForm]);
+  });
+
+  // 处理图片文件,将每个图片文件对象添加到 FormData 中
+  const imageFile = formData.images[0]?.raw; // 获取 raw 属性中的 File 对象
+  if (imageFile instanceof File) {
+    bodyData.append('images', imageFile); // 使用 'images' 作为字段名,将 File 对象添加到 FormData 中
+  }
+  await useResponse(bodyData, api.createEmployee);
+}
+
+// 取消操作
+const handleCancel = () => {
+  showDialog.value = false;
+};
+
+onMounted(() => {
+})
+</script>
+
+<template>
+  <el-dialog v-model="showDialog" :close-on-click-modal="false" title="新增电脑信息" width="35%">
+    <el-form
+        ref="ruleFormRef"
+        :model="formData"
+        :rules="rules"
+        class="employee-info-form"
+        label-width="auto"
+        status-icon>
+      <el-form-item label="姓名:" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入姓名" />
+      </el-form-item>
+      <el-form-item label="部门:" prop="department">
+        <el-input v-model="formData.department" placeholder="请输入部门" />
+      </el-form-item>
+      <el-form-item label="电话号码:" prop="phone">
+        <el-input v-model="formData.phone" placeholder="请输入电话号码" />
+      </el-form-item>
+      <el-form-item label="邮箱:" prop="email">
+        <el-input v-model="formData.email" placeholder="请输入邮箱" />
+      </el-form-item>
+      <!-- 人员图片上传 -->
+      <el-form-item :key="images" label="人员图片:">
+        <el-upload
+            ref="upload"
+            v-model:file-list="formData.images"
+            :auto-upload="false"
+            :limit="1"
+            :on-exceed="handleExceed"
+            action="#"
+            list-type="picture-card"
+        >
+          <el-icon>
+            <Plus />
+          </el-icon>
+        </el-upload>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button type="primary" @click="submitForm(ruleFormRef)">保存</el-button>
+        <el-button @click="handleCancel">取消</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<style scoped>
+.dialog-footer {
+  margin-bottom: 10px;
+  text-align: center;
+}
+</style>

+ 142 - 0
src/views/employee-information/components/EditEmployeeInfo.vue

@@ -0,0 +1,142 @@
+<script lang="ts" setup>
+/**
+ * @Name: EditEmployeeInfo.vue
+ * @Description: 编辑电脑信息
+ * @Author: xinyan
+ */
+import { ref } from 'vue';
+import { FormInstance, FormRules, UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
+import { genFileId } from 'element-plus';
+import router from '/@/router';
+
+const visible = defineModel({ default: false });
+const { employeeInfo, name } = defineProps<{
+  employeeInfo: any;
+  name: any;
+}>();
+
+// 图片处理
+const upload = ref<UploadInstance>();
+// 表单数据
+interface RuleForm {
+  name: string;
+  department: string;
+  phone: string;
+  email: string;
+  user: string;
+  macaddress: string;
+  ipaddress: string;
+  images: UploadFile[];
+}
+
+const ruleFormRef = ref<FormInstance>();
+const formData = reactive<RuleForm>({
+  name: '',
+  department: '',
+  phone: '',
+  email: '',
+  images: [],
+});
+
+const rules = reactive<FormRules<RuleForm>>({
+  name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+  department: [{ required: true, message: '请输入部门', trigger: 'blur' }],
+  phone: [{ required: true, message: '请输入电话号码', trigger: 'blur' }],
+  email: [{ required: true, message: '请输入所属商铺', trigger: 'blur' }],
+});
+
+onBeforeMount(() => {
+  replaceCol();
+});
+
+// 超过图片限制时触发的回调
+const handleExceed: UploadProps['onExceed'] = (files) => {
+  upload.value!.clearFiles();
+  const file = files[0] as UploadRawFile;
+  file.uid = genFileId();
+  upload.value!.handleStart(file);
+};
+
+// 保存操作
+const handleSave = () => {
+  console.log(formData);
+};
+
+// 取消操作
+const handleCancel = () => {
+  router.push({
+    path: '/employee',
+  });
+};
+
+function replaceCol() {
+  const result = Object.keys(formData).reduce((acc, key) => {
+    if (key in employeeInfo) {
+      acc[key] = employeeInfo[key];
+    }
+    return acc;
+  }, {} as { [key: string]: any });
+  Object.assign(formData, result);
+}
+
+onMounted(() => {
+  console.log('visible',visible);
+})
+</script>
+
+<template>
+  <el-drawer :title="`人员编辑 - ${name}`" v-model="visible" size="30%">
+    <el-form  ref="ruleFormRef" :model="formData" :rules="rules" class="employee-info-form" label-width="auto">
+      <el-form-item label="姓名:" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入姓名" />
+      </el-form-item>
+      <el-form-item label="部门:" prop="department">
+        <el-input v-model="formData.department" placeholder="请输入部门" />
+      </el-form-item>
+      <el-form-item label="电话号码:" prop="phone">
+        <el-input v-model="formData.phone" placeholder="请输入电话号码" />
+      </el-form-item>
+      <el-form-item label="邮箱:" prop="email">
+        <el-input v-model="formData.email" placeholder="请输入邮箱" />
+      </el-form-item>
+      <!-- 电脑图片上传 -->
+      <el-form-item :key="images" label="电脑图片:">
+        <el-upload
+            ref="upload"
+            v-model:file-list="formData.images"
+            :auto-upload="false"
+            :limit="1"
+            :on-exceed="handleExceed"
+            action="#"
+            list-type="picture-card"
+        >
+          <el-icon>
+            <Plus />
+          </el-icon>
+        </el-upload>
+      </el-form-item>
+
+      <!-- 按钮组 -->
+      <el-form-item class="button-group">
+        <el-button type="primary" @click="handleSave">保存</el-button>
+        <el-button @click="handleCancel">取消</el-button>
+      </el-form-item>
+    </el-form>
+  </el-drawer>
+</template>
+
+<style scoped>
+.employee-info-form {
+  padding: 20px;
+}
+
+.button-group {
+  margin-top: 20px;
+  text-align: center;
+}
+
+.employee-info-form :deep(.el-form-item__label) {
+  font-size: 14px;
+  font-weight: 500;
+}
+</style>

+ 193 - 0
src/views/employee-information/components/EmployeeDetail.vue

@@ -0,0 +1,193 @@
+<script lang="ts" setup>
+/**
+ * @Name: EmployeeDetail.vue
+ * @Description: 电脑信息-当前
+ * @Author: xinyan
+ */
+import { useResponse } from '/@/utils/useResponse';
+import { EmployeeShopColumns, EmployeeComputerColumns } from '/@/views/employee-information/useColumns';
+import * as api from '../api';
+import { Picture as IconPicture } from '@element-plus/icons-vue';
+import { useTableData } from '/@/utils/useTableData';
+import { usePagination } from '/@/utils/usePagination';
+
+const route = useRoute();
+const id = route.query.id;
+const employeeOverview: any = ref([]);
+const overviewLoading = ref();
+const currentView = ref('shop');
+const { tableOptions, handlePageChange } = usePagination(fetchEmployeeData);
+
+const gridOptions: any = reactive({
+  border: 'inner',
+  round: true,
+  stripe: true,
+  shopRowHighLight: true,
+  height: 700,
+  toolbarConfig: {
+    custom: true,
+    slots: {
+      buttons: 'toolbar_buttons',
+      // tools: 'toolbar_tools'
+    },
+  },
+  rowConfig: {
+    isHover: true,
+  },
+  columnConfig: {
+    resizable: true,
+  },
+  pagerConfig: {
+    total: tableOptions.value.total,
+    page: tableOptions.value.page,
+    limit: tableOptions.value.limit,
+  },
+  loading: false,
+  loadingConfig: {
+    icon: 'vxe-icon-indicator roll',
+    text: '正在拼命加载中...',
+  },
+  columns: EmployeeShopColumns,
+  data: [],
+});
+
+function pageChange({ pageSize, shopPage }: any) {
+  gridOptions.pagerConfig.limit = pageSize;
+  gridOptions.pagerConfig.page = shopPage;
+  fetchEmployeeData();
+}
+
+// 当前信息、历史记录
+async function fetchEmployeeData(view) { // 默认为当前视图
+  const query = {
+    page: gridOptions.pagerConfig.page,
+    limit: gridOptions.pagerConfig.limit,
+  };
+
+  switch (view) {
+    case 'shop':
+      gridOptions.columns = EmployeeShopColumns;
+      currentView.value = 'shop';
+      query.id = id;
+      await useTableData(api.getShopTableData, query, gridOptions);
+      break;
+    case 'computer':
+      gridOptions.columns = EmployeeComputerColumns;
+      currentView.value = 'computer';
+      query.id = id;
+      await useTableData(api.getComputerTableData, query, gridOptions);
+      break;
+  }
+}
+
+function switchView(view) {
+  if (view !== currentView.value) { // 只有在不同的视图时才切换
+    fetchEmployeeData(view); // 调用 fetchEmployeeData 来加载新的视图数据
+  }
+}
+
+async function fetchEmployeeDetailOverview() {
+  const res = await useResponse(id, api.getEmployeeDetailOverview, overviewLoading);
+  employeeOverview.value = res.data;
+}
+
+const getImageSrc = () => {
+  // 如果 `images` 有值,则返回第一张图片的 URL;否则返回占位图
+  return employeeOverview.value.images && employeeOverview.value.images.length > 0
+      ? employeeOverview.value.images[0].image_url
+      : 'https://via.placeholder.com/150';
+};
+
+// 表格样式
+const cellStyle = () => {
+  return {
+    fontSize: '12px',
+    fontWeight: '600',
+  };
+};
+
+const headerCellStyle = () => {
+  return {
+    fontSize: '12px',
+  };
+};
+
+onMounted(() => {
+  fetchEmployeeData(currentView.value);
+  fetchEmployeeDetailOverview();
+});
+</script>
+
+<template>
+  <div class="p-2.5">
+    <!-- overview-card -->
+    <el-card v-loading="overviewLoading" body-class="flex items-center" shadow="hover" style="border: none">
+      <el-image :src="getImageSrc()" class="mr-7 rounded-2xl" style="height: 100px; width: 100px; object-fit: contain">
+        <template #error>
+          <div class="mr-3.5 flex items-center justify-center text-5xl" style="height: 100px; width: 100px; background-color: #f5f5f5">
+            <el-icon>
+              <icon-picture />
+            </el-icon>
+          </div>
+        </template>
+      </el-image>
+      <el-col :span="18">
+        <div class="info-container text-lg">
+          <div class="info-column">
+            <div class="font-semibold">
+              姓名:
+              <span class="font-medium italic ml-1.5" style="color: #64748b">{{ employeeOverview.name }}</span>
+            </div>
+            <div class="font-semibold">
+              所属部门:
+              <span class="font-medium italic ml-1.5" style="color: #64748b">{{ employeeOverview.department }}</span>
+            </div>
+            <div class="font-semibold">
+              电话:
+              <span class="font-medium italic ml-1.5" style="color: #64748b">{{ employeeOverview.phone }}</span>
+            </div>
+            <div class="font-semibold">
+              邮箱:
+              <span class="font-medium italic ml-1.5" style="color: #64748b">{{ employeeOverview.email }}</span>
+            </div>
+          </div>
+        </div>
+      </el-col>
+    </el-card>
+    <!-- table-card -->
+    <el-card body-style="padding-top: 10px" class="mt-2.5" shadow="hover" style="border: none">
+      <vxe-grid :cell-style="cellStyle" :header-cell-style="headerCellStyle" v-bind="gridOptions">
+        <template #toolbar_buttons>
+          <el-button :type="currentView === 'shop' ? 'primary' : 'default'" @click="switchView('shop')"> 店铺信息 </el-button>
+          <el-button :type="currentView === 'computer' ? 'primary' : 'default'" @click="switchView('computer')"> 电脑信息 </el-button>
+        </template>
+        <template #pager>
+          <vxe-pager
+              v-model:currentPage="gridOptions.pagerConfig.page"
+              v-model:pageSize="gridOptions.pagerConfig.limit"
+              :total="gridOptions.pagerConfig.total"
+              size="small"
+              @page-change="handlePageChange"
+          >
+          </vxe-pager>
+        </template>
+      </vxe-grid>
+    </el-card>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.info-container {
+  display: flex;
+  justify-content: space-between;
+}
+
+.info-column {
+  flex: 1;
+  padding: 0 10px;
+}
+
+p {
+  margin: 5px 0;
+}
+</style>

+ 141 - 0
src/views/employee-information/components/InfoCard.vue

@@ -0,0 +1,141 @@
+<script setup lang="ts">/**
+ * @Name: InfoCard.vue
+ * @Description:
+ * @Author: xinyan
+ */
+import { useRouter } from 'vue-router';
+import { ref } from 'vue';
+import { usePagination } from '/@/utils/usePagination';
+import { useTableData } from '/@/utils/useTableData';
+import { Delete, EditPen, Picture as IconPicture, Search } from '@element-plus/icons-vue';
+import * as api from '/@/views/employee-information/api';
+import { useResponse } from '/@/utils/useResponse';
+import EditEmployeeInfo from '/@/views/employee-information/components/EditEmployeeInfo.vue';
+
+const router = useRouter();
+const loading = ref();
+const isDrawerVisible = ref(false);
+const employeeInfo = ref([]);
+const { tableOptions, handlePageChange } = usePagination(fetchCardData);
+tableOptions.value.limit = 12;
+
+async function fetchCardData() {
+  const query = {
+    page: tableOptions.value.page,
+    limit: tableOptions.value.limit,
+  };
+  await useTableData(api.getCardData, query, tableOptions);
+}
+
+const checkItem = (item) => {
+  router.push({
+    path: '/employee/detail',
+    query: {
+      id: item.id,
+    },
+  });
+};
+
+async function editItem (item) {
+  const res = await useResponse(item.id, api.getEmployeeDetailOverview);
+  employeeInfo.value = res.data;
+  if (employeeInfo.value){
+    isDrawerVisible.value = true;  // 显示 Drawer
+    console.log("=>(InfoCard.vue:44) isDrawerVisible.value", isDrawerVisible.value);
+  }
+}
+
+const getImageUrl = (images) => {
+  // 如果有图片,返回第一个图片的 image_url,否则返回占位图
+  return images.length > 0 ? images[0].image_url : '';
+};
+
+onMounted(() => {
+  fetchCardData();
+});
+
+</script>
+
+<template>
+  <el-card shadow="never" style="border: none; min-height: 820px; position: relative;">
+    <div class="card-container">
+      <el-row :gutter="20">
+        <el-col v-for="(item, index) in tableOptions.data" :key="index" :span="4" class="my-2.5">
+          <el-card class="item-card" shadow="hover">
+            <el-image :src="getImageUrl(item.images)" alt="电脑图片" class="card-image">
+              <template #error>
+                <el-icon class="card-image" style="font-size: 4rem">
+                  <icon-picture />
+                </el-icon>
+              </template>
+            </el-image>
+            <div class="card-content">
+              <div>
+                <span style="color: #808d97;font-weight: 500">姓名: </span>
+                <span style="font-weight: 500">{{ item.name }}</span>
+              </div>
+              <div>
+                <span style="color: #808d97;font-weight: 500">部门: </span>
+                <span style="font-weight: 500">{{ item.department }}</span>
+              </div>
+            </div>
+            <div class="card-footer">
+              <el-button :icon="Search" circle text bg type="primary" @click="checkItem(item)" />
+              <el-button :icon="EditPen" circle text bg type="warning" @click="editItem(item)" />
+              <el-button :icon="Delete" circle text bg type="danger" @click="deleteItem(item)" />
+            </div>
+          </el-card>
+        </el-col>
+      </el-row>
+    </div>
+    <div class="pagination-container" style="position: absolute; right: 32px; bottom: 32px;">
+      <el-pagination
+          v-model:current-page="tableOptions.page"
+          v-model:page-size="tableOptions.limit"
+          :page-sizes="[6, 12, 24,36,48,60]"
+          :total="tableOptions.total"
+          background
+          layout="sizes, prev, pager, next, total"
+          @change="handlePageChange"
+      />
+    </div>
+    <!-- 编辑 Drawer -->
+    <EditEmployeeInfo v-if="isDrawerVisible" v-model="isDrawerVisible" :name="employeeInfo.name" :employeeInfo/>
+  </el-card>
+
+</template>
+
+<style scoped lang="scss">
+.card-container {
+  margin-bottom: 20px;
+}
+
+.item-card {
+  border-radius: 10px;
+  overflow: hidden;
+  position: relative;
+}
+
+.card-image {
+  width: 100%;
+  height: 150px;
+  object-fit: cover;
+}
+
+.card-content {
+  padding: 10px;
+  font-size: 14px;
+}
+
+.card-footer {
+  display: flex;
+  justify-content: flex-end;
+  // padding: 10px;
+}
+
+.pagination-container {
+  display: flex;
+  justify-content: flex-end;
+  // margin-bottom: 20px;
+}
+</style>

+ 27 - 5
src/views/employee-information/index.vue

@@ -1,16 +1,38 @@
-<script setup lang="ts">
+<script lang="ts" setup>
 /**
  * @Name: index.vue
  * @Description: 员工信息页面
  * @Author: Cheney
  */
 
+import InfoCard from '/@/views/employee-information/components/InfoCard.vue';
+import { Plus } from '@element-plus/icons-vue';
+import CreateEmployee from '/@/views/employee-information/components/CreateEmployee.vue';
+
+// 创建弹窗
+const showDialog = ref(false);
+
+async function addEmployee() {
+  showDialog.value = true;
+}
 </script>
 
 <template>
-
+	<div class="px-2.5">
+    <el-card class="my-2.5" shadow="never" style="border: none;">
+      <div class="flex justify-between items-baseline">
+        <div>
+          <span class="font-bold text-xl">人员信息概览</span>
+          <el-divider class=" text-3xl" direction="vertical"/>
+        </div>
+        <span>
+           <el-button :icon="Plus" text bg type="primary" @click="addEmployee">添 加</el-button>
+        </span>
+      </div>
+    </el-card>
+		<InfoCard />
+    <CreateEmployee v-model="showDialog"/>
+	</div>
 </template>
 
-<style scoped>
-
-</style>
+<style scoped></style>

+ 17 - 0
src/views/employee-information/useColumns.tsx

@@ -0,0 +1,17 @@
+export const EmployeeShopColumns = [
+  { field: 'platformNumber', title: '平台编号'},
+  { field: 'country', title: '店铺区域'},
+  { field: 'platform', title: '平台'},
+  { field: 'brandName', title: '品牌'},
+  { field: 'company', title: '所属公司'},
+  { field: 'line', title: '线路'},
+  { field: 'ipaddress', title: 'IP地址'},
+];
+
+export const EmployeeComputerColumns =[
+  { field: 'computerNumber', title: '电脑编号'},
+  { field: 'station', title: '工位号'},
+  { field: 'computerType', title: '电脑类型'},
+  { field: 'ipaddress', title: 'IP地址'},
+  { field: 'macaddress', title: 'MAC地址'},
+]