|
@@ -0,0 +1,816 @@
|
|
|
|
|
+<!-- @Author: Antigravity -->
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="p-2">
|
|
|
|
|
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter"
|
|
|
|
|
+ :leave-active-class="proxy?.animate.searchAnimate.leave">
|
|
|
|
|
+ <div v-show="showSearch" class="search">
|
|
|
|
|
+ <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="85px">
|
|
|
|
|
+ <el-form-item label="姓名" prop="name">
|
|
|
|
|
+ <el-input v-model="queryParams.name" placeholder="请输入姓名" clearable @keyup.enter="handleQuery" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="手机号" prop="phone">
|
|
|
|
|
+ <el-input v-model="queryParams.phone" placeholder="请输入手机号" clearable @keyup.enter="handleQuery" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
|
|
|
|
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </transition>
|
|
|
|
|
+
|
|
|
|
|
+ <el-card shadow="never">
|
|
|
|
|
+ <template #header>
|
|
|
|
|
+ <el-row :gutter="10" class="mb8">
|
|
|
|
|
+ <el-col :span="1.5">
|
|
|
|
|
+ <el-button v-hasPermi="['employee:employee:add']" type="primary" icon="Plus"
|
|
|
|
|
+ @click="handleAdd">新增</el-button>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <el-table v-loading="loading" :data="employeeList" border @selection-change="handleSelectionChange">
|
|
|
|
|
+ <el-table-column type="selection" width="55" align="center" />
|
|
|
|
|
+ <el-table-column label="序号" align="center" width="70" type="index" />
|
|
|
|
|
+ <el-table-column label="姓名" align="center" prop="name" min-width="120" />
|
|
|
|
|
+ <el-table-column label="头像" align="center" width="80">
|
|
|
|
|
+ <template #default="scope">
|
|
|
|
|
+ <el-avatar :size="36" :src="scope.row.avatarUrl" icon="UserFilled" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="手机" align="center" prop="phone" width="140" />
|
|
|
|
|
+ <el-table-column label="注册时间" align="center" prop="createTime" width="170" />
|
|
|
|
|
+ <el-table-column label="状态" align="center" width="100">
|
|
|
|
|
+ <template #default="scope">
|
|
|
|
|
+ <el-switch v-if="checkPermi(['employee:employee:changeStatus'])" v-model="scope.row._statusActive"
|
|
|
|
|
+ :loading="scope.row._statusLoading" inline-prompt @change="handleStatusChange(scope.row)" />
|
|
|
|
|
+ <el-tag v-else :type="scope.row.status === '0' ? 'danger' : 'success'">
|
|
|
|
|
+ {{ scope.row.status === '0' ? '禁用' : '启用' }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
|
|
|
|
|
+ <template #default="scope">
|
|
|
|
|
+ <el-button v-hasPermi="['employee:employee:auth']" link type="success" icon="User"
|
|
|
|
|
+ @click="handleAuth(scope.row)">授权</el-button>
|
|
|
|
|
+ <el-button v-hasPermi="['employee:employee:query']" link type="primary" icon="View"
|
|
|
|
|
+ @click="handleDetail(scope.row)">详情</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+
|
|
|
|
|
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
|
|
|
|
|
+ v-model:limit="queryParams.pageSize" @pagination="getList" />
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 授权客户对话框 -->
|
|
|
|
|
+ <SelectClientDialog v-model="authDialog.visible" v-model:selected-clients="selectedAuthClients" title="授权客户"
|
|
|
|
|
+ selected-label="已授权" placeholder-text="输入客户名称搜索并添加授权客户" confirm-text="确认授权" :search-results="authSearchResults"
|
|
|
|
|
+ :search-loading="authSearchLoading" :confirm-loading="buttonLoading" :searched="authSearched"
|
|
|
|
|
+ @search="handleAuthSearch" @confirm="confirmAuth" />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 新增员工对话框 -->
|
|
|
|
|
+ <el-dialog v-model="addDialog.visible" title="新增员工" width="520px" append-to-body @close="resetAddForm"
|
|
|
|
|
+ class="add-employee-dialog">
|
|
|
|
|
+ <div class="add-form-wrapper">
|
|
|
|
|
+ <!-- 头像区 -->
|
|
|
|
|
+ <div class="add-avatar-area">
|
|
|
|
|
+ <el-upload class="add-avatar-uploader" :action="uploadUrl" :headers="uploadHeaders" :show-file-list="false"
|
|
|
|
|
+ :on-success="handleAddAvatarSuccess" :before-upload="beforeAddAvatarUpload">
|
|
|
|
|
+ <div class="add-avatar-mask">
|
|
|
|
|
+ <img v-if="addForm.avatarUrl" :src="addForm.avatarUrl" class="add-avatar-img" />
|
|
|
|
|
+ <el-icon v-else class="add-avatar-placeholder">
|
|
|
|
|
+ <UserFilled />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ <div class="add-avatar-overlay">
|
|
|
|
|
+ <el-icon>
|
|
|
|
|
+ <Camera />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ <span>更换头像</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-upload>
|
|
|
|
|
+ <p class="add-avatar-tip">点击上传员工头像</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form ref="addFormRef" :model="addForm" :rules="addFormRules" label-position="top" class="add-form">
|
|
|
|
|
+ <el-form-item label="姓名" prop="name">
|
|
|
|
|
+ <el-input v-model="addForm.name" placeholder="请输入员工姓名" clearable maxlength="30" size="large" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="电话(登录账号)" prop="phone">
|
|
|
|
|
+ <el-input v-model="addForm.phone" placeholder="请输入手机号作为登录账号" clearable maxlength="11" size="large" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="授权客户">
|
|
|
|
|
+ <div class="add-auth-wrap">
|
|
|
|
|
+ <div v-if="addSelectedClients.length > 0" class="add-auth-tags">
|
|
|
|
|
+ <el-tag v-for="(client, idx) in addSelectedClients" :key="idx" closable size="large"
|
|
|
|
|
+ @close="removeAddClient(idx)" class="add-auth-tag">
|
|
|
|
|
+ <span class="add-auth-tag-index">{{ idx + 1 }}</span>
|
|
|
|
|
+ {{ client.name }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-button class="add-auth-btn" @click="openAddSelectClient">
|
|
|
|
|
+ <el-icon>
|
|
|
|
|
+ <Plus />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ {{ addSelectedClients.length > 0 ? '继续添加' : '选择客户' }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <div class="add-dialog-footer">
|
|
|
|
|
+ <el-button size="large" @click="addDialog.visible = false">取 消</el-button>
|
|
|
|
|
+ <el-button size="large" type="primary" :loading="addBtnLoading" @click="confirmAdd">确认新增</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 新增员工-选择授权客户子对话框 -->
|
|
|
|
|
+ <SelectClientDialog v-model="selectClientVisible" v-model:selected-clients="addSelectedClients"
|
|
|
|
|
+ :search-results="selectClientResults" :search-loading="selectClientLoading" :searched="selectClientSearched"
|
|
|
|
|
+ @search="handleSelectClientSearch" @confirm="selectClientVisible = false" />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 员工详情对话框 -->
|
|
|
|
|
+ <el-dialog v-model="detailDialog.visible" title="员工详情" width="680px" append-to-body>
|
|
|
|
|
+ <template v-if="detailDialog.data">
|
|
|
|
|
+ <div class="detail-header">
|
|
|
|
|
+ <el-avatar :size="72" :src="detailDialog.data.avatarUrl" icon="UserFilled" class="detail-avatar" />
|
|
|
|
|
+ <div class="detail-name-box">
|
|
|
|
|
+ <text class="detail-name">{{ detailDialog.data.name }}</text>
|
|
|
|
|
+ <el-tag :type="detailDialog.data.status === '0' ? 'danger' : 'success'" size="small"
|
|
|
|
|
+ class="detail-status-tag">
|
|
|
|
|
+ {{ detailDialog.data.status === '0' ? '已禁用' : '已启用' }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="detail-divider"></div>
|
|
|
|
|
+ <el-descriptions :column="2" border size="small" class="detail-descriptions">
|
|
|
|
|
+ <el-descriptions-item label="员工ID">{{ detailDialog.data.id }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="手机号">{{ detailDialog.data.phone || '-' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="注册时间">{{ detailDialog.data.createTime }}</el-descriptions-item>
|
|
|
|
|
+ </el-descriptions>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="section-title">授权客户</div>
|
|
|
|
|
+ <div v-if="detailDialog.data.authClientList && detailDialog.data.authClientList.length > 0">
|
|
|
|
|
+ <div class="auth-client-card" v-for="(client, idx) in detailDialog.data.authClientList" :key="idx">
|
|
|
|
|
+ <span class="client-index">{{ idx + 1 }}</span>
|
|
|
|
|
+ <div class="client-info">
|
|
|
|
|
+ <div class="client-row"><span class="client-label">名称</span><span class="client-value">{{ client.name
|
|
|
|
|
+ }}</span></div>
|
|
|
|
|
+ <div class="client-row"><span class="client-label">类型</span><span class="client-value">{{
|
|
|
|
|
+ client.clientClass || '-' }}</span></div>
|
|
|
|
|
+ <div class="client-row"><span class="client-label">加入时间</span><span class="client-value">{{
|
|
|
|
|
+ client.enterDate || '-' }}</span></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-empty v-else description="暂无授权客户" :image-size="60" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup name="Customer" lang="ts">
|
|
|
|
|
+import { listEmployee, addEmployee, getEmployeeDetail, authEmployee, changeEmployeeStatus } from '@/api/system/employee';
|
|
|
|
|
+import { EmployeeVO, EmployeeQuery, EmployeeForm, ErpClientBriefVO } from '@/api/system/employee/types';
|
|
|
|
|
+import { searchErpClient, getErpClientByIds } from '@/api/erp/client';
|
|
|
|
|
+import { ErpClientVO } from '@/api/erp/client/types';
|
|
|
|
|
+import { checkPermi } from '@/utils/permission';
|
|
|
|
|
+import { globalHeaders } from '@/utils/request';
|
|
|
|
|
+import SelectClientDialog from './components/SelectClientDialog.vue';
|
|
|
|
|
+
|
|
|
|
|
+/** @Author: Antigravity */
|
|
|
|
|
+
|
|
|
|
|
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
|
|
+
|
|
|
|
|
+const employeeList = ref<EmployeeVO[]>([]);
|
|
|
|
|
+const buttonLoading = ref(false);
|
|
|
|
|
+const loading = ref(true);
|
|
|
|
|
+const showSearch = ref(true);
|
|
|
|
|
+const ids = ref<Array<string | number>>([]);
|
|
|
|
|
+const total = ref(0);
|
|
|
|
|
+
|
|
|
|
|
+const queryFormRef = ref<ElFormInstance>();
|
|
|
|
|
+
|
|
|
|
|
+// 授权客户对话框相关
|
|
|
|
|
+const authDialog = reactive({
|
|
|
|
|
+ visible: false,
|
|
|
|
|
+ employeeId: undefined as string | number | undefined
|
|
|
|
|
+});
|
|
|
|
|
+const authSearchLoading = ref(false);
|
|
|
|
|
+const authSearchResults = ref<ErpClientVO[]>([]);
|
|
|
|
|
+const authSearched = ref(false);
|
|
|
|
|
+const selectedAuthClients = ref<ErpClientVO[]>([]);
|
|
|
|
|
+
|
|
|
|
|
+// 详情对话框
|
|
|
|
|
+const detailDialog = reactive({
|
|
|
|
|
+ visible: false,
|
|
|
|
|
+ data: null as EmployeeVO | null
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 新增员工对话框
|
|
|
|
|
+const addDialog = reactive({
|
|
|
|
|
+ visible: false
|
|
|
|
|
+});
|
|
|
|
|
+const addFormRef = ref<ElFormInstance>();
|
|
|
|
|
+const addForm = reactive({
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ phone: '',
|
|
|
|
|
+ avatar: undefined as string | undefined,
|
|
|
|
|
+ avatarUrl: ''
|
|
|
|
|
+});
|
|
|
|
|
+const addFormRules = reactive({
|
|
|
|
|
+ name: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
|
|
|
|
|
+ phone: [{ required: true, message: '手机号不能为空', trigger: 'blur' }]
|
|
|
|
|
+});
|
|
|
|
|
+const addBtnLoading = ref(false);
|
|
|
|
|
+const addSelectedClients = ref<ErpClientVO[]>([]);
|
|
|
|
|
+
|
|
|
|
|
+// 新增员工-选择授权客户子对话框
|
|
|
|
|
+const selectClientVisible = ref(false);
|
|
|
|
|
+const selectClientLoading = ref(false);
|
|
|
|
|
+const selectClientResults = ref<ErpClientVO[]>([]);
|
|
|
|
|
+const selectClientSearched = ref(false);
|
|
|
|
|
+
|
|
|
|
|
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
|
|
|
|
|
+const uploadHeaders = ref(globalHeaders());
|
|
|
|
|
+
|
|
|
|
|
+watch(() => authDialog.visible, (val) => {
|
|
|
|
|
+ if (val) initAuthDialog();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const data = reactive<PageData<EmployeeForm, EmployeeQuery>>({
|
|
|
|
|
+ form: {} as any,
|
|
|
|
|
+ queryParams: {
|
|
|
|
|
+ pageNum: 1,
|
|
|
|
|
+ pageSize: 10,
|
|
|
|
|
+ name: undefined,
|
|
|
|
|
+ phone: undefined,
|
|
|
|
|
+ wechatOpenid: undefined
|
|
|
|
|
+ },
|
|
|
|
|
+ rules: {}
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const { queryParams } = toRefs(data);
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 处理列表数据,附加状态展示字段
|
|
|
|
|
+ */
|
|
|
|
|
+const processList = (list: EmployeeVO[]) => {
|
|
|
|
|
+ list.forEach(item => {
|
|
|
|
|
+ (item as any)._statusActive = item.status !== '0';
|
|
|
|
|
+ (item as any)._statusLoading = false;
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 查询员工列表 */
|
|
|
|
|
+const getList = async () => {
|
|
|
|
|
+ loading.value = true;
|
|
|
|
|
+ const res = await listEmployee(queryParams.value);
|
|
|
|
|
+ const rows = res.rows as EmployeeVO[] || [];
|
|
|
|
|
+ processList(rows);
|
|
|
|
|
+ employeeList.value = rows;
|
|
|
|
|
+ total.value = res.total;
|
|
|
|
|
+ loading.value = false;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 搜索按钮操作 */
|
|
|
|
|
+const handleQuery = () => {
|
|
|
|
|
+ queryParams.value.pageNum = 1;
|
|
|
|
|
+ getList();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 重置按钮操作 */
|
|
|
|
|
+const resetQuery = () => {
|
|
|
|
|
+ queryFormRef.value?.resetFields();
|
|
|
|
|
+ handleQuery();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 多选框选中数据 */
|
|
|
|
|
+const handleSelectionChange = (selection: EmployeeVO[]) => {
|
|
|
|
|
+ ids.value = selection.map((item) => item.id);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 状态切换 */
|
|
|
|
|
+const handleStatusChange = async (row: EmployeeVO & { _statusActive: boolean; _statusLoading: boolean }) => {
|
|
|
|
|
+ const newStatus = row._statusActive ? '1' : '0';
|
|
|
|
|
+ try {
|
|
|
|
|
+ row._statusLoading = true;
|
|
|
|
|
+ await changeEmployeeStatus(row.id, newStatus);
|
|
|
|
|
+ proxy?.$modal.msgSuccess(newStatus === '1' ? '已启用' : '已禁用');
|
|
|
|
|
+ } catch (e: any) {
|
|
|
|
|
+ // 失败回滚状态
|
|
|
|
|
+ row._statusActive = !row._statusActive;
|
|
|
|
|
+ proxy?.$modal.msgError(e?.msg || '操作失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ row._statusLoading = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 授权员工按钮操作 */
|
|
|
|
|
+const handleAuth = (row: EmployeeVO) => {
|
|
|
|
|
+ authDialog.employeeId = row.id;
|
|
|
|
|
+ authDialog.visible = true;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 弹窗打开时初始化 */
|
|
|
|
|
+const initAuthDialog = async () => {
|
|
|
|
|
+ authSearchResults.value = [];
|
|
|
|
|
+ authSearched.value = false;
|
|
|
|
|
+ selectedAuthClients.value = [];
|
|
|
|
|
+ if (!authDialog.employeeId) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getEmployeeDetail(authDialog.employeeId);
|
|
|
|
|
+ const ids = res.data?.authClientFRowIDs;
|
|
|
|
|
+ if (ids) {
|
|
|
|
|
+ selectedAuthClients.value = await fetchClientsByIds(ids);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('加载已授权客户失败', e);
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 搜索ERP客户 */
|
|
|
|
|
+const handleAuthSearch = async (keyword: string) => {
|
|
|
|
|
+ if (!keyword) {
|
|
|
|
|
+ proxy?.$modal.msgWarning('请输入客户名称');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ authSearchLoading.value = true;
|
|
|
|
|
+ authSearched.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await searchErpClient(keyword);
|
|
|
|
|
+ authSearchResults.value = res.data || [];
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ authSearchResults.value = [];
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ authSearchLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 确认授权 */
|
|
|
|
|
+const confirmAuth = async () => {
|
|
|
|
|
+ if (!selectedAuthClients.value || selectedAuthClients.value.length === 0) {
|
|
|
|
|
+ proxy?.$modal.msgError('请至少选择一个ERP客户');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ buttonLoading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const authClientFRowIDs = selectedAuthClients.value.map(c => c.rowId).join(',');
|
|
|
|
|
+ await authEmployee(authDialog.employeeId!, authClientFRowIDs);
|
|
|
|
|
+ proxy?.$modal.msgSuccess('授权成功');
|
|
|
|
|
+ authDialog.visible = false;
|
|
|
|
|
+ getList();
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ buttonLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 查看员工详情 */
|
|
|
|
|
+const handleDetail = async (row: EmployeeVO) => {
|
|
|
|
|
+ detailDialog.visible = true;
|
|
|
|
|
+ detailDialog.data = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getEmployeeDetail(row.id);
|
|
|
|
|
+ const data = res.data;
|
|
|
|
|
+ if (data && data.authClientFRowIDs) {
|
|
|
|
|
+ data.authClientList = await fetchClientsByIds(data.authClientFRowIDs) as any;
|
|
|
|
|
+ }
|
|
|
|
|
+ detailDialog.data = data;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ proxy?.$modal.msgError('获取员工详情失败');
|
|
|
|
|
+ detailDialog.visible = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 新增按钮操作 */
|
|
|
|
|
+const handleAdd = () => {
|
|
|
|
|
+ resetAddForm();
|
|
|
|
|
+ addDialog.visible = true;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 重置新增表单 */
|
|
|
|
|
+const resetAddForm = () => {
|
|
|
|
|
+ addForm.name = '';
|
|
|
|
|
+ addForm.phone = '';
|
|
|
|
|
+ addForm.avatar = undefined;
|
|
|
|
|
+ addForm.avatarUrl = '';
|
|
|
|
|
+ addSelectedClients.value = [];
|
|
|
|
|
+ addFormRef.value?.resetFields();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 打开选择授权客户子对话框 */
|
|
|
|
|
+const openAddSelectClient = () => {
|
|
|
|
|
+ selectClientVisible.value = true;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 搜索客户 */
|
|
|
|
|
+const handleSelectClientSearch = async (keyword: string) => {
|
|
|
|
|
+ if (!keyword) {
|
|
|
|
|
+ proxy?.$modal.msgWarning('请输入客户名称');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ selectClientLoading.value = true;
|
|
|
|
|
+ selectClientSearched.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await searchErpClient(keyword);
|
|
|
|
|
+ selectClientResults.value = res.data || [];
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ selectClientResults.value = [];
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ selectClientLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 头像上传成功回调 */
|
|
|
|
|
+const handleAddAvatarSuccess = (res: any) => {
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ addForm.avatar = res.data.ossId;
|
|
|
|
|
+ addForm.avatarUrl = res.data.url;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ proxy?.$modal.msgError(res.msg || '上传失败');
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 头像上传前校验 */
|
|
|
|
|
+const beforeAddAvatarUpload = (file: any) => {
|
|
|
|
|
+ const isImg = file.type.indexOf('image/') > -1;
|
|
|
|
|
+ if (!isImg) {
|
|
|
|
|
+ proxy?.$modal.msgError('请上传图片格式文件');
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const isLt5M = file.size / 1024 / 1024 < 5;
|
|
|
|
|
+ if (!isLt5M) {
|
|
|
|
|
+ proxy?.$modal.msgError('上传头像图片大小不能超过 5MB!');
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 确认新增员工 */
|
|
|
|
|
+const confirmAdd = async () => {
|
|
|
|
|
+ const valid = await addFormRef.value?.validate().catch(() => false);
|
|
|
|
|
+ if (!valid) return;
|
|
|
|
|
+ addBtnLoading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data: EmployeeForm = {
|
|
|
|
|
+ name: addForm.name,
|
|
|
|
|
+ phone: addForm.phone,
|
|
|
|
|
+ password: '123456',
|
|
|
|
|
+ authClientFRowIDs: addSelectedClients.value.map(c => c.rowId).join(',')
|
|
|
|
|
+ };
|
|
|
|
|
+ if (addForm.avatar) {
|
|
|
|
|
+ data.avatar = addForm.avatar;
|
|
|
|
|
+ }
|
|
|
|
|
+ await addEmployee(data);
|
|
|
|
|
+ proxy?.$modal.msgSuccess('新增成功');
|
|
|
|
|
+ addDialog.visible = false;
|
|
|
|
|
+ getList();
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ addBtnLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 根据 authClientFRowIDs 逗号分隔字符串,调用 ERP 批量接口反查客户列表
|
|
|
|
|
+ */
|
|
|
|
|
+const fetchClientsByIds = async (authClientFRowIDs: string): Promise<ErpClientVO[]> => {
|
|
|
|
|
+ const ids = authClientFRowIDs.split(',').map(s => s.trim()).filter(Boolean);
|
|
|
|
|
+ if (ids.length === 0) return [];
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getErpClientByIds(ids.join(','));
|
|
|
|
|
+ return (res.data || []).map(c => ({
|
|
|
|
|
+ ...c,
|
|
|
|
|
+ num: (c as any).num || c.rowId
|
|
|
|
|
+ }));
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('批量查询ERP客户失败', e);
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ getList();
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+/* ========== 详情弹窗 ========== */
|
|
|
|
|
+.section-title {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ margin: 20px 0 12px 0;
|
|
|
|
|
+ padding-left: 10px;
|
|
|
|
|
+ border-left: 3px solid #C1001C;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.el-dialog .pagination-container) {
|
|
|
|
|
+ display: flex !important;
|
|
|
|
|
+ justify-content: center !important;
|
|
|
|
|
+ background-color: transparent !important;
|
|
|
|
|
+ padding: 20px 0 10px 0 !important;
|
|
|
|
|
+ margin-top: 0 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 24px;
|
|
|
|
|
+ padding-bottom: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-avatar {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-name-box {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-name {
|
|
|
|
|
+ font-size: 22px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1d1d1f;
|
|
|
|
|
+ letter-spacing: 0.5px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-status-tag {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-divider {
|
|
|
|
|
+ height: 1px;
|
|
|
|
|
+ background: linear-gradient(90deg, #eee 0%, transparent 100%);
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-descriptions {
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.auth-client-card {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ padding: 16px 20px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ border: 1px solid #f0f0f0;
|
|
|
|
|
+ transition: box-shadow 0.2s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.auth-client-card:hover {
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.client-index {
|
|
|
|
|
+ width: 28px;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ background: #C1001C;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ margin-top: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.client-info {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.client-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.client-label {
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ min-width: 60px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.client-value {
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ========== 新增员工对话框 ========== */
|
|
|
|
|
+.add-employee-dialog :deep(.el-dialog__header) {
|
|
|
|
|
+ padding: 24px 28px 0;
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-employee-dialog :deep(.el-dialog__title) {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1a1a1a;
|
|
|
|
|
+ letter-spacing: 0.3px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-employee-dialog :deep(.el-dialog__body) {
|
|
|
|
|
+ padding: 12px 28px 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-form-wrapper {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-avatar-area {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 28px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-avatar-uploader {
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-avatar-mask {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: 88px;
|
|
|
|
|
+ height: 88px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: #f5f7fa;
|
|
|
|
|
+ border: 2px solid #e8ecf1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ transition: border-color 0.3s, box-shadow 0.3s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-avatar-mask:hover {
|
|
|
|
|
+ border-color: #409eff;
|
|
|
|
|
+ box-shadow: 0 0 0 4px rgba(64, 158, 255, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-avatar-mask:hover .add-avatar-overlay {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-avatar-img {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-avatar-placeholder {
|
|
|
|
|
+ font-size: 36px;
|
|
|
|
|
+ color: #c0c4cc;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-avatar-overlay {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.45);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 2px;
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transition: opacity 0.3s;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-avatar-overlay .el-icon {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-avatar-tip {
|
|
|
|
|
+ margin: 10px 0 0;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #b0b5be;
|
|
|
|
|
+ letter-spacing: 0.2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-form {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-form :deep(.el-form-item__label) {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #4a4f5a;
|
|
|
|
|
+ padding-bottom: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-form :deep(.el-input--large .el-input__wrapper) {
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0 0 0 1px #e4e7ed inset;
|
|
|
|
|
+ transition: box-shadow 0.25s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-form :deep(.el-input--large .el-input__wrapper:hover) {
|
|
|
|
|
+ box-shadow: 0 0 0 1px #c6cacf inset;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-form :deep(.el-input--large.is-focus .el-input__wrapper) {
|
|
|
|
|
+ box-shadow: 0 0 0 1px #409eff inset, 0 0 0 3px rgba(64, 158, 255, 0.08);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-auth-wrap {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-auth-tags {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-auth-tag {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ padding: 6px 12px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ background: #f0f5ff;
|
|
|
|
|
+ color: #3370ff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-auth-tag :deep(.el-tag__close) {
|
|
|
|
|
+ color: #8fa8e0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-auth-tag :deep(.el-tag__close:hover) {
|
|
|
|
|
+ background: rgba(51, 112, 255, 0.12);
|
|
|
|
|
+ color: #3370ff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-auth-tag-index {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ width: 18px;
|
|
|
|
|
+ height: 18px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #3370ff;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ margin-right: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-auth-btn {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ border: 1px dashed #c0c4cc;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ padding: 8px 16px;
|
|
|
|
|
+ transition: border-color 0.25s, color 0.25s, background 0.25s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-auth-btn:hover {
|
|
|
|
|
+ border-color: #409eff;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ background: #f0f5ff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-dialog-footer {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ padding-top: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-dialog-footer .el-button {
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 10px 28px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ letter-spacing: 0.5px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-dialog-footer .el-button--primary {
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.25);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-dialog-footer .el-button--primary:hover {
|
|
|
|
|
+ box-shadow: 0 4px 16px rgba(64, 158, 255, 0.35);
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|