index.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. <!-- @Author: Antigravity -->
  2. <template>
  3. <div class="p-2">
  4. <transition :enter-active-class="proxy?.animate.searchAnimate.enter"
  5. :leave-active-class="proxy?.animate.searchAnimate.leave">
  6. <div v-show="showSearch" class="search">
  7. <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="85px">
  8. <el-form-item label="姓名" prop="name">
  9. <el-input v-model="queryParams.name" placeholder="请输入姓名" clearable @keyup.enter="handleQuery" />
  10. </el-form-item>
  11. <el-form-item label="手机号" prop="phone">
  12. <el-input v-model="queryParams.phone" placeholder="请输入手机号" clearable @keyup.enter="handleQuery" />
  13. </el-form-item>
  14. <el-form-item>
  15. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  16. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  17. </el-form-item>
  18. </el-form>
  19. </div>
  20. </transition>
  21. <el-card shadow="never">
  22. <template #header>
  23. <el-row :gutter="10" class="mb8">
  24. <el-col :span="1.5">
  25. <el-button v-hasPermi="['employee:employee:add']" type="primary" icon="Plus"
  26. @click="handleAdd">新增</el-button>
  27. </el-col>
  28. <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
  29. </el-row>
  30. </template>
  31. <el-table v-loading="loading" :data="employeeList" border @selection-change="handleSelectionChange">
  32. <el-table-column type="selection" width="55" align="center" />
  33. <el-table-column label="序号" align="center" width="70" type="index" />
  34. <el-table-column label="姓名" align="center" prop="name" min-width="120" />
  35. <el-table-column label="头像" align="center" width="80">
  36. <template #default="scope">
  37. <el-avatar :size="36" :src="scope.row.avatarUrl" icon="UserFilled" />
  38. </template>
  39. </el-table-column>
  40. <el-table-column label="手机" align="center" prop="phone" width="140" />
  41. <el-table-column label="注册时间" align="center" prop="createTime" width="170" />
  42. <el-table-column label="状态" align="center" width="100">
  43. <template #default="scope">
  44. <el-switch v-if="checkPermi(['employee:employee:changeStatus'])" v-model="scope.row._statusActive"
  45. :loading="scope.row._statusLoading" inline-prompt @change="handleStatusChange(scope.row)" />
  46. <el-tag v-else :type="scope.row.status === '0' ? 'danger' : 'success'">
  47. {{ scope.row.status === '0' ? '禁用' : '启用' }}
  48. </el-tag>
  49. </template>
  50. </el-table-column>
  51. <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
  52. <template #default="scope">
  53. <el-button v-hasPermi="['employee:employee:auth']" link type="success" icon="User"
  54. @click="handleAuth(scope.row)">授权</el-button>
  55. <el-button v-hasPermi="['employee:employee:query']" link type="primary" icon="View"
  56. @click="handleDetail(scope.row)">详情</el-button>
  57. </template>
  58. </el-table-column>
  59. </el-table>
  60. <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
  61. v-model:limit="queryParams.pageSize" @pagination="getList" />
  62. </el-card>
  63. <!-- 授权客户对话框 -->
  64. <SelectClientDialog v-model="authDialog.visible" v-model:selected-clients="selectedAuthClients" title="授权客户"
  65. selected-label="已授权" placeholder-text="输入客户名称搜索并添加授权客户" confirm-text="确认授权" :search-results="authSearchResults"
  66. :search-loading="authSearchLoading" :confirm-loading="buttonLoading" :searched="authSearched"
  67. @search="handleAuthSearch" @confirm="confirmAuth" />
  68. <!-- 新增员工对话框 -->
  69. <el-dialog v-model="addDialog.visible" title="新增员工" width="520px" append-to-body @close="resetAddForm"
  70. class="add-employee-dialog">
  71. <div class="add-form-wrapper">
  72. <!-- 头像区 -->
  73. <div class="add-avatar-area">
  74. <el-upload class="add-avatar-uploader" :action="uploadUrl" :headers="uploadHeaders" :show-file-list="false"
  75. :on-success="handleAddAvatarSuccess" :before-upload="beforeAddAvatarUpload">
  76. <div class="add-avatar-mask">
  77. <img v-if="addForm.avatarUrl" :src="addForm.avatarUrl" class="add-avatar-img" />
  78. <el-icon v-else class="add-avatar-placeholder">
  79. <UserFilled />
  80. </el-icon>
  81. <div class="add-avatar-overlay">
  82. <el-icon>
  83. <Camera />
  84. </el-icon>
  85. <span>更换头像</span>
  86. </div>
  87. </div>
  88. </el-upload>
  89. <p class="add-avatar-tip">点击上传员工头像</p>
  90. </div>
  91. <el-form ref="addFormRef" :model="addForm" :rules="addFormRules" label-position="top" class="add-form">
  92. <el-form-item label="姓名" prop="name">
  93. <el-input v-model="addForm.name" placeholder="请输入员工姓名" clearable maxlength="30" size="large" />
  94. </el-form-item>
  95. <el-form-item label="电话(登录账号)" prop="phone">
  96. <el-input v-model="addForm.phone" placeholder="请输入手机号作为登录账号" clearable maxlength="11" size="large" />
  97. </el-form-item>
  98. <el-form-item label="授权客户">
  99. <div class="add-auth-wrap">
  100. <div v-if="addSelectedClients.length > 0" class="add-auth-tags">
  101. <el-tag v-for="(client, idx) in addSelectedClients" :key="idx" closable size="large"
  102. @close="removeAddClient(idx)" class="add-auth-tag">
  103. <span class="add-auth-tag-index">{{ idx + 1 }}</span>
  104. {{ client.name }}
  105. </el-tag>
  106. </div>
  107. <el-button class="add-auth-btn" @click="openAddSelectClient">
  108. <el-icon>
  109. <Plus />
  110. </el-icon>
  111. {{ addSelectedClients.length > 0 ? '继续添加' : '选择客户' }}
  112. </el-button>
  113. </div>
  114. </el-form-item>
  115. </el-form>
  116. </div>
  117. <template #footer>
  118. <div class="add-dialog-footer">
  119. <el-button size="large" @click="addDialog.visible = false">取 消</el-button>
  120. <el-button size="large" type="primary" :loading="addBtnLoading" @click="confirmAdd">确认新增</el-button>
  121. </div>
  122. </template>
  123. </el-dialog>
  124. <!-- 新增员工-选择授权客户子对话框 -->
  125. <SelectClientDialog v-model="selectClientVisible" v-model:selected-clients="addSelectedClients"
  126. :search-results="selectClientResults" :search-loading="selectClientLoading" :searched="selectClientSearched"
  127. @search="handleSelectClientSearch" @confirm="selectClientVisible = false" />
  128. <!-- 员工详情对话框 -->
  129. <el-dialog v-model="detailDialog.visible" title="员工详情" width="680px" append-to-body>
  130. <template v-if="detailDialog.data">
  131. <div class="detail-header">
  132. <el-avatar :size="72" :src="detailDialog.data.avatarUrl" icon="UserFilled" class="detail-avatar" />
  133. <div class="detail-name-box">
  134. <text class="detail-name">{{ detailDialog.data.name }}</text>
  135. <el-tag :type="detailDialog.data.status === '0' ? 'danger' : 'success'" size="small"
  136. class="detail-status-tag">
  137. {{ detailDialog.data.status === '0' ? '已禁用' : '已启用' }}
  138. </el-tag>
  139. </div>
  140. </div>
  141. <div class="detail-divider"></div>
  142. <el-descriptions :column="2" border size="small" class="detail-descriptions">
  143. <el-descriptions-item label="员工ID">{{ detailDialog.data.id }}</el-descriptions-item>
  144. <el-descriptions-item label="手机号">{{ detailDialog.data.phone || '-' }}</el-descriptions-item>
  145. <el-descriptions-item label="注册时间">{{ detailDialog.data.createTime }}</el-descriptions-item>
  146. </el-descriptions>
  147. <div class="section-title">授权客户</div>
  148. <div v-if="detailDialog.data.authClientList && detailDialog.data.authClientList.length > 0">
  149. <div class="auth-client-card" v-for="(client, idx) in detailDialog.data.authClientList" :key="idx">
  150. <span class="client-index">{{ idx + 1 }}</span>
  151. <div class="client-info">
  152. <div class="client-row"><span class="client-label">名称</span><span class="client-value">{{ client.name
  153. }}</span></div>
  154. <div class="client-row"><span class="client-label">类型</span><span class="client-value">{{
  155. client.clientClass || '-' }}</span></div>
  156. <div class="client-row"><span class="client-label">加入时间</span><span class="client-value">{{
  157. client.enterDate || '-' }}</span></div>
  158. </div>
  159. </div>
  160. </div>
  161. <el-empty v-else description="暂无授权客户" :image-size="60" />
  162. </template>
  163. </el-dialog>
  164. </div>
  165. </template>
  166. <script setup name="Customer" lang="ts">
  167. import { listEmployee, addEmployee, getEmployeeDetail, authEmployee, changeEmployeeStatus } from '@/api/system/employee';
  168. import { EmployeeVO, EmployeeQuery, EmployeeForm, ErpClientBriefVO } from '@/api/system/employee/types';
  169. import { searchErpClient, getErpClientByIds } from '@/api/erp/client';
  170. import { ErpClientVO } from '@/api/erp/client/types';
  171. import { checkPermi } from '@/utils/permission';
  172. import { globalHeaders } from '@/utils/request';
  173. import SelectClientDialog from './components/SelectClientDialog.vue';
  174. /** @Author: Antigravity */
  175. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  176. const employeeList = ref<EmployeeVO[]>([]);
  177. const buttonLoading = ref(false);
  178. const loading = ref(true);
  179. const showSearch = ref(true);
  180. const ids = ref<Array<string | number>>([]);
  181. const total = ref(0);
  182. const queryFormRef = ref<ElFormInstance>();
  183. // 授权客户对话框相关
  184. const authDialog = reactive({
  185. visible: false,
  186. employeeId: undefined as string | number | undefined
  187. });
  188. const authSearchLoading = ref(false);
  189. const authSearchResults = ref<ErpClientVO[]>([]);
  190. const authSearched = ref(false);
  191. const selectedAuthClients = ref<ErpClientVO[]>([]);
  192. // 详情对话框
  193. const detailDialog = reactive({
  194. visible: false,
  195. data: null as EmployeeVO | null
  196. });
  197. // 新增员工对话框
  198. const addDialog = reactive({
  199. visible: false
  200. });
  201. const addFormRef = ref<ElFormInstance>();
  202. const addForm = reactive({
  203. name: '',
  204. phone: '',
  205. avatar: undefined as string | undefined,
  206. avatarUrl: ''
  207. });
  208. const addFormRules = reactive({
  209. name: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
  210. phone: [{ required: true, message: '手机号不能为空', trigger: 'blur' }]
  211. });
  212. const addBtnLoading = ref(false);
  213. const addSelectedClients = ref<ErpClientVO[]>([]);
  214. // 新增员工-选择授权客户子对话框
  215. const selectClientVisible = ref(false);
  216. const selectClientLoading = ref(false);
  217. const selectClientResults = ref<ErpClientVO[]>([]);
  218. const selectClientSearched = ref(false);
  219. const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
  220. const uploadHeaders = ref(globalHeaders());
  221. watch(() => authDialog.visible, (val) => {
  222. if (val) initAuthDialog();
  223. });
  224. const data = reactive<PageData<EmployeeForm, EmployeeQuery>>({
  225. form: {} as any,
  226. queryParams: {
  227. pageNum: 1,
  228. pageSize: 10,
  229. name: undefined,
  230. phone: undefined,
  231. wechatOpenid: undefined
  232. },
  233. rules: {}
  234. });
  235. const { queryParams } = toRefs(data);
  236. /**
  237. * 处理列表数据,附加状态展示字段
  238. */
  239. const processList = (list: EmployeeVO[]) => {
  240. list.forEach(item => {
  241. (item as any)._statusActive = item.status !== '0';
  242. (item as any)._statusLoading = false;
  243. });
  244. };
  245. /** 查询员工列表 */
  246. const getList = async () => {
  247. loading.value = true;
  248. const res = await listEmployee(queryParams.value);
  249. const rows = res.rows as EmployeeVO[] || [];
  250. processList(rows);
  251. employeeList.value = rows;
  252. total.value = res.total;
  253. loading.value = false;
  254. };
  255. /** 搜索按钮操作 */
  256. const handleQuery = () => {
  257. queryParams.value.pageNum = 1;
  258. getList();
  259. };
  260. /** 重置按钮操作 */
  261. const resetQuery = () => {
  262. queryFormRef.value?.resetFields();
  263. handleQuery();
  264. };
  265. /** 多选框选中数据 */
  266. const handleSelectionChange = (selection: EmployeeVO[]) => {
  267. ids.value = selection.map((item) => item.id);
  268. };
  269. /** 状态切换 */
  270. const handleStatusChange = async (row: EmployeeVO & { _statusActive: boolean; _statusLoading: boolean }) => {
  271. const newStatus = row._statusActive ? '1' : '0';
  272. try {
  273. row._statusLoading = true;
  274. await changeEmployeeStatus(row.id, newStatus);
  275. proxy?.$modal.msgSuccess(newStatus === '1' ? '已启用' : '已禁用');
  276. } catch (e: any) {
  277. // 失败回滚状态
  278. row._statusActive = !row._statusActive;
  279. proxy?.$modal.msgError(e?.msg || '操作失败');
  280. } finally {
  281. row._statusLoading = false;
  282. }
  283. };
  284. /** 授权员工按钮操作 */
  285. const handleAuth = (row: EmployeeVO) => {
  286. authDialog.employeeId = row.id;
  287. authDialog.visible = true;
  288. };
  289. /** 弹窗打开时初始化 */
  290. const initAuthDialog = async () => {
  291. authSearchResults.value = [];
  292. authSearched.value = false;
  293. selectedAuthClients.value = [];
  294. if (!authDialog.employeeId) return;
  295. try {
  296. const res = await getEmployeeDetail(authDialog.employeeId);
  297. const ids = res.data?.authClientFRowIDs;
  298. if (ids) {
  299. selectedAuthClients.value = await fetchClientsByIds(ids);
  300. }
  301. } catch (e) {
  302. console.error('加载已授权客户失败', e);
  303. }
  304. };
  305. /** 搜索ERP客户 */
  306. const handleAuthSearch = async (keyword: string) => {
  307. if (!keyword) {
  308. proxy?.$modal.msgWarning('请输入客户名称');
  309. return;
  310. }
  311. authSearchLoading.value = true;
  312. authSearched.value = true;
  313. try {
  314. const res = await searchErpClient(keyword);
  315. authSearchResults.value = res.data || [];
  316. } catch (e) {
  317. authSearchResults.value = [];
  318. } finally {
  319. authSearchLoading.value = false;
  320. }
  321. };
  322. /** 确认授权 */
  323. const confirmAuth = async () => {
  324. if (!selectedAuthClients.value || selectedAuthClients.value.length === 0) {
  325. proxy?.$modal.msgError('请至少选择一个ERP客户');
  326. return;
  327. }
  328. buttonLoading.value = true;
  329. try {
  330. const authClientFRowIDs = selectedAuthClients.value.map(c => c.rowId).join(',');
  331. await authEmployee(authDialog.employeeId!, authClientFRowIDs);
  332. proxy?.$modal.msgSuccess('授权成功');
  333. authDialog.visible = false;
  334. getList();
  335. } finally {
  336. buttonLoading.value = false;
  337. }
  338. };
  339. /** 查看员工详情 */
  340. const handleDetail = async (row: EmployeeVO) => {
  341. detailDialog.visible = true;
  342. detailDialog.data = null;
  343. try {
  344. const res = await getEmployeeDetail(row.id);
  345. const data = res.data;
  346. if (data && data.authClientFRowIDs) {
  347. data.authClientList = await fetchClientsByIds(data.authClientFRowIDs) as any;
  348. }
  349. detailDialog.data = data;
  350. } catch (e) {
  351. proxy?.$modal.msgError('获取员工详情失败');
  352. detailDialog.visible = false;
  353. }
  354. };
  355. /** 新增按钮操作 */
  356. const handleAdd = () => {
  357. resetAddForm();
  358. addDialog.visible = true;
  359. };
  360. /** 重置新增表单 */
  361. const resetAddForm = () => {
  362. addForm.name = '';
  363. addForm.phone = '';
  364. addForm.avatar = undefined;
  365. addForm.avatarUrl = '';
  366. addSelectedClients.value = [];
  367. addFormRef.value?.resetFields();
  368. };
  369. /** 打开选择授权客户子对话框 */
  370. const openAddSelectClient = () => {
  371. selectClientVisible.value = true;
  372. };
  373. /** 搜索客户 */
  374. const handleSelectClientSearch = async (keyword: string) => {
  375. if (!keyword) {
  376. proxy?.$modal.msgWarning('请输入客户名称');
  377. return;
  378. }
  379. selectClientLoading.value = true;
  380. selectClientSearched.value = true;
  381. try {
  382. const res = await searchErpClient(keyword);
  383. selectClientResults.value = res.data || [];
  384. } catch (e) {
  385. selectClientResults.value = [];
  386. } finally {
  387. selectClientLoading.value = false;
  388. }
  389. };
  390. /** 头像上传成功回调 */
  391. const handleAddAvatarSuccess = (res: any) => {
  392. if (res.code === 200) {
  393. addForm.avatar = res.data.ossId;
  394. addForm.avatarUrl = res.data.url;
  395. } else {
  396. proxy?.$modal.msgError(res.msg || '上传失败');
  397. }
  398. };
  399. /** 头像上传前校验 */
  400. const beforeAddAvatarUpload = (file: any) => {
  401. const isImg = file.type.indexOf('image/') > -1;
  402. if (!isImg) {
  403. proxy?.$modal.msgError('请上传图片格式文件');
  404. return false;
  405. }
  406. const isLt5M = file.size / 1024 / 1024 < 5;
  407. if (!isLt5M) {
  408. proxy?.$modal.msgError('上传头像图片大小不能超过 5MB!');
  409. return false;
  410. }
  411. return true;
  412. };
  413. /** 确认新增员工 */
  414. const confirmAdd = async () => {
  415. const valid = await addFormRef.value?.validate().catch(() => false);
  416. if (!valid) return;
  417. addBtnLoading.value = true;
  418. try {
  419. const data: EmployeeForm = {
  420. name: addForm.name,
  421. phone: addForm.phone,
  422. password: '123456',
  423. authClientFRowIDs: addSelectedClients.value.map(c => c.rowId).join(',')
  424. };
  425. if (addForm.avatar) {
  426. data.avatar = addForm.avatar;
  427. }
  428. await addEmployee(data);
  429. proxy?.$modal.msgSuccess('新增成功');
  430. addDialog.visible = false;
  431. getList();
  432. } finally {
  433. addBtnLoading.value = false;
  434. }
  435. };
  436. /**
  437. * 根据 authClientFRowIDs 逗号分隔字符串,调用 ERP 批量接口反查客户列表
  438. */
  439. const fetchClientsByIds = async (authClientFRowIDs: string): Promise<ErpClientVO[]> => {
  440. const ids = authClientFRowIDs.split(',').map(s => s.trim()).filter(Boolean);
  441. if (ids.length === 0) return [];
  442. try {
  443. const res = await getErpClientByIds(ids.join(','));
  444. return (res.data || []).map(c => ({
  445. ...c,
  446. num: (c as any).num || c.rowId
  447. }));
  448. } catch (e) {
  449. console.error('批量查询ERP客户失败', e);
  450. return [];
  451. }
  452. };
  453. onMounted(() => {
  454. getList();
  455. });
  456. </script>
  457. <style scoped>
  458. /* ========== 详情弹窗 ========== */
  459. .section-title {
  460. font-size: 14px;
  461. font-weight: 600;
  462. color: #303133;
  463. margin: 20px 0 12px 0;
  464. padding-left: 10px;
  465. border-left: 3px solid #C1001C;
  466. }
  467. :deep(.el-dialog .pagination-container) {
  468. display: flex !important;
  469. justify-content: center !important;
  470. background-color: transparent !important;
  471. padding: 20px 0 10px 0 !important;
  472. margin-top: 0 !important;
  473. }
  474. .detail-header {
  475. display: flex;
  476. align-items: center;
  477. gap: 24px;
  478. padding-bottom: 20px;
  479. }
  480. .detail-avatar {
  481. flex-shrink: 0;
  482. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  483. }
  484. .detail-name-box {
  485. display: flex;
  486. align-items: center;
  487. gap: 12px;
  488. }
  489. .detail-name {
  490. font-size: 22px;
  491. font-weight: 600;
  492. color: #1d1d1f;
  493. letter-spacing: 0.5px;
  494. }
  495. .detail-status-tag {
  496. flex-shrink: 0;
  497. }
  498. .detail-divider {
  499. height: 1px;
  500. background: linear-gradient(90deg, #eee 0%, transparent 100%);
  501. margin-bottom: 20px;
  502. }
  503. .detail-descriptions {
  504. margin-bottom: 8px;
  505. }
  506. .auth-client-card {
  507. display: flex;
  508. align-items: flex-start;
  509. gap: 16px;
  510. background: #fafafa;
  511. border-radius: 12px;
  512. padding: 16px 20px;
  513. margin-bottom: 12px;
  514. border: 1px solid #f0f0f0;
  515. transition: box-shadow 0.2s;
  516. }
  517. .auth-client-card:hover {
  518. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  519. }
  520. .client-index {
  521. width: 28px;
  522. height: 28px;
  523. background: #C1001C;
  524. color: #fff;
  525. border-radius: 50%;
  526. display: flex;
  527. align-items: center;
  528. justify-content: center;
  529. font-size: 13px;
  530. font-weight: 600;
  531. flex-shrink: 0;
  532. margin-top: 2px;
  533. }
  534. .client-info {
  535. flex: 1;
  536. display: flex;
  537. flex-direction: column;
  538. gap: 6px;
  539. }
  540. .client-row {
  541. display: flex;
  542. gap: 12px;
  543. font-size: 13px;
  544. }
  545. .client-label {
  546. color: #999;
  547. min-width: 60px;
  548. flex-shrink: 0;
  549. }
  550. .client-value {
  551. color: #333;
  552. font-weight: 500;
  553. }
  554. /* ========== 新增员工对话框 ========== */
  555. .add-employee-dialog :deep(.el-dialog__header) {
  556. padding: 24px 28px 0;
  557. margin: 0;
  558. }
  559. .add-employee-dialog :deep(.el-dialog__title) {
  560. font-size: 18px;
  561. font-weight: 600;
  562. color: #1a1a1a;
  563. letter-spacing: 0.3px;
  564. }
  565. .add-employee-dialog :deep(.el-dialog__body) {
  566. padding: 12px 28px 20px;
  567. }
  568. .add-form-wrapper {
  569. display: flex;
  570. flex-direction: column;
  571. align-items: center;
  572. }
  573. .add-avatar-area {
  574. display: flex;
  575. flex-direction: column;
  576. align-items: center;
  577. margin-bottom: 28px;
  578. }
  579. .add-avatar-uploader {
  580. cursor: pointer;
  581. }
  582. .add-avatar-mask {
  583. position: relative;
  584. width: 88px;
  585. height: 88px;
  586. border-radius: 50%;
  587. overflow: hidden;
  588. background: #f5f7fa;
  589. border: 2px solid #e8ecf1;
  590. display: flex;
  591. align-items: center;
  592. justify-content: center;
  593. transition: border-color 0.3s, box-shadow 0.3s;
  594. }
  595. .add-avatar-mask:hover {
  596. border-color: #409eff;
  597. box-shadow: 0 0 0 4px rgba(64, 158, 255, 0.1);
  598. }
  599. .add-avatar-mask:hover .add-avatar-overlay {
  600. opacity: 1;
  601. }
  602. .add-avatar-img {
  603. width: 100%;
  604. height: 100%;
  605. object-fit: cover;
  606. }
  607. .add-avatar-placeholder {
  608. font-size: 36px;
  609. color: #c0c4cc;
  610. }
  611. .add-avatar-overlay {
  612. position: absolute;
  613. inset: 0;
  614. background: rgba(0, 0, 0, 0.45);
  615. display: flex;
  616. flex-direction: column;
  617. align-items: center;
  618. justify-content: center;
  619. gap: 2px;
  620. opacity: 0;
  621. transition: opacity 0.3s;
  622. color: #fff;
  623. font-size: 11px;
  624. }
  625. .add-avatar-overlay .el-icon {
  626. font-size: 18px;
  627. }
  628. .add-avatar-tip {
  629. margin: 10px 0 0;
  630. font-size: 12px;
  631. color: #b0b5be;
  632. letter-spacing: 0.2px;
  633. }
  634. .add-form {
  635. width: 100%;
  636. }
  637. .add-form :deep(.el-form-item__label) {
  638. font-size: 13px;
  639. font-weight: 500;
  640. color: #4a4f5a;
  641. padding-bottom: 6px;
  642. }
  643. .add-form :deep(.el-input--large .el-input__wrapper) {
  644. border-radius: 8px;
  645. box-shadow: 0 0 0 1px #e4e7ed inset;
  646. transition: box-shadow 0.25s;
  647. }
  648. .add-form :deep(.el-input--large .el-input__wrapper:hover) {
  649. box-shadow: 0 0 0 1px #c6cacf inset;
  650. }
  651. .add-form :deep(.el-input--large.is-focus .el-input__wrapper) {
  652. box-shadow: 0 0 0 1px #409eff inset, 0 0 0 3px rgba(64, 158, 255, 0.08);
  653. }
  654. .add-auth-wrap {
  655. display: flex;
  656. flex-wrap: wrap;
  657. align-items: center;
  658. gap: 8px;
  659. }
  660. .add-auth-tags {
  661. display: flex;
  662. flex-wrap: wrap;
  663. gap: 6px;
  664. }
  665. .add-auth-tag {
  666. font-size: 13px;
  667. border-radius: 6px;
  668. padding: 6px 12px;
  669. border: none;
  670. background: #f0f5ff;
  671. color: #3370ff;
  672. }
  673. .add-auth-tag :deep(.el-tag__close) {
  674. color: #8fa8e0;
  675. }
  676. .add-auth-tag :deep(.el-tag__close:hover) {
  677. background: rgba(51, 112, 255, 0.12);
  678. color: #3370ff;
  679. }
  680. .add-auth-tag-index {
  681. display: inline-flex;
  682. align-items: center;
  683. justify-content: center;
  684. width: 18px;
  685. height: 18px;
  686. border-radius: 50%;
  687. background: #3370ff;
  688. color: #fff;
  689. font-size: 11px;
  690. font-weight: 600;
  691. margin-right: 6px;
  692. }
  693. .add-auth-btn {
  694. flex-shrink: 0;
  695. border-radius: 6px;
  696. border: 1px dashed #c0c4cc;
  697. background: #fff;
  698. color: #606266;
  699. font-size: 13px;
  700. padding: 8px 16px;
  701. transition: border-color 0.25s, color 0.25s, background 0.25s;
  702. }
  703. .add-auth-btn:hover {
  704. border-color: #409eff;
  705. color: #409eff;
  706. background: #f0f5ff;
  707. }
  708. .add-dialog-footer {
  709. display: flex;
  710. justify-content: flex-end;
  711. gap: 12px;
  712. padding-top: 4px;
  713. }
  714. .add-dialog-footer .el-button {
  715. border-radius: 8px;
  716. padding: 10px 28px;
  717. font-size: 14px;
  718. font-weight: 500;
  719. letter-spacing: 0.5px;
  720. }
  721. .add-dialog-footer .el-button--primary {
  722. box-shadow: 0 2px 8px rgba(64, 158, 255, 0.25);
  723. }
  724. .add-dialog-footer .el-button--primary:hover {
  725. box-shadow: 0 4px 16px rgba(64, 158, 255, 0.35);
  726. }
  727. </style>