enterprise.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. <template>
  2. <div class="me-enterprise">
  3. <div class="enterprise-header">
  4. <h2 class="enterprise-title">我的企业</h2>
  5. </div>
  6. <div class="enterprise-content">
  7. <!-- 加载状态 -->
  8. <div v-if="loading" class="loading-state">
  9. <el-icon class="loading-icon">
  10. <Loading />
  11. </el-icon>
  12. <p>加载中...</p>
  13. </div>
  14. <!-- 如果没有企业,显示创建企业提示 -->
  15. <div v-else-if="!hasEnterprise" class="empty-state">
  16. <div class="empty-icon">
  17. <el-icon class="empty-icon__icon">
  18. <OfficeBuilding />
  19. </el-icon>
  20. </div>
  21. <p class="empty-text">您还没有加入任何企业</p>
  22. <el-button type="primary" @click="openCreateDialog">
  23. 创建企业
  24. </el-button>
  25. </div>
  26. <!-- 如果有企业,显示企业员工列表 -->
  27. <div v-else class="enterprise-list">
  28. <!-- 企业信息展示 -->
  29. <div class="list-header">
  30. <div class="enterprise-info">
  31. <div class="enterprise-name">
  32. <el-icon class="enterprise-name-icon">
  33. <OfficeBuilding />
  34. </el-icon>
  35. <span class="name-text">{{ currentEnterpriseName }}</span>
  36. </div>
  37. <p class="hint-text">注:一个账号只能归属一个企业</p>
  38. </div>
  39. <div class="header-actions">
  40. <el-button type="primary" :icon="Plus" @click="showAddMemberDialog = true">
  41. 添加企业成员
  42. </el-button>
  43. </div>
  44. </div>
  45. <div style="margin-bottom: 10px;">
  46. <el-button type="primary" size="small" plain @click="handleOpenProductSeatDialog">产品分配</el-button>
  47. </div>
  48. <!-- 员工列表表格 -->
  49. <div class="table-section">
  50. <el-table ref="employeeTable" :data="employeeList" style="width: 100%"
  51. :header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '500' }" empty-text="暂无员工数据"
  52. >
  53. <el-table-column prop="id" label="编号" width="100" align="center" />
  54. <el-table-column prop="name" label="姓名" min-width="150" align="center" />
  55. <el-table-column prop="email" label="邮箱" min-width="200" align="center" />
  56. <el-table-column prop="role_text" label="角色" width="140" align="center">
  57. <template #default="{ row }">
  58. <el-tag :type="row.role === 1 ? 'success' : 'info'" size="small">
  59. {{ row.role_text }}
  60. </el-tag>
  61. </template>
  62. </el-table-column>
  63. <el-table-column prop="status" label="状态" width="120" align="center">
  64. <template #default="{ row }">
  65. <el-tag :type="getStatusType(row.status)" size="small">
  66. {{ row.status_text }}
  67. </el-tag>
  68. </template>
  69. </el-table-column>
  70. <el-table-column prop="join_time" label="加入时间" width="160" align="center" />
  71. <el-table-column label="操作" width="100" align="center" fixed="right">
  72. <template #default="{ row }">
  73. <el-button type="danger" size="small" :disabled="!row.can_operate || row.id === currentUserId"
  74. @click="handleDeleteMember(row)">
  75. 删除
  76. </el-button>
  77. </template>
  78. </el-table-column>
  79. </el-table>
  80. </div>
  81. <!-- 分页控件 -->
  82. <div v-if="employeeList.length > 0 || total > 0" class="pagination-section">
  83. <el-pagination :current-page="currentPage" :page-size="pageSize" :page-sizes="pageSizes" :total="total"
  84. layout="total, sizes, prev, pager, next, jumper" background @size-change="handlePageSizeChange"
  85. @current-change="handleCurrentPageChange" />
  86. </div>
  87. </div>
  88. </div>
  89. <!-- 创建企业弹框 -->
  90. <CreateEnterpriseDialog v-model:visible="showCreateDialog" @success="handleCreateSuccess"
  91. @close="handleDialogClose" />
  92. <!-- 添加企业成员弹框 -->
  93. <AddMemberDialog v-model:visible="showAddMemberDialog" :company-id="currentCompanyId" @success="loadEnterpriseList"
  94. @close="showAddMemberDialog = false" />
  95. <!-- 产品分配席位弹框 -->
  96. <ProductSeatDialog v-model:visible="showProductSeatDialog" :member-list="employeeList"
  97. :selected-member-ids="selectedMemberIdsForDialog" @success="handleProductSeatSuccess"
  98. @close="handleProductSeatDialogClose" />
  99. </div>
  100. </template>
  101. <script setup lang="ts">
  102. import { computed, onMounted, ref, watch } from 'vue'
  103. import { storeToRefs } from 'pinia'
  104. import { ElMessage, ElMessageBox } from 'element-plus'
  105. import { Loading, OfficeBuilding, Plus } from '@element-plus/icons-vue'
  106. import { useUserInfo } from '/@/stores/userInfo'
  107. import CreateEnterpriseDialog from '../CreateEnterpriseDialog.vue'
  108. import AddMemberDialog from '../AddMemberDialog.vue'
  109. import ProductSeatDialog from '../ProductSeatDialog.vue'
  110. import { checkCompany, myCompanies, members, removeMember } from '/@/api/frontend/me'
  111. defineOptions({ name: 'MeEnterprise' })
  112. type Employee = {
  113. id: number
  114. name: string // 企业名称
  115. email: string // 电子邮箱
  116. role: number // 角色:1创建人/管理员,2普通成员
  117. status: number // 状态:1正常,2禁用
  118. join_time: string // 加入时间
  119. create_time: string // 创建时间
  120. role_text: string // 角色文本
  121. status_text: string // 状态文本
  122. join_time_formatted: string // 格式化的加入时间
  123. create_time_formatted: string // 格式化的创建时间
  124. can_operate: boolean // 是否可以操作(只有创建人可以操作)
  125. }
  126. type ProductSeatResult = {
  127. mode?: 'assign' | 'revoke'
  128. memberIds: number[]
  129. }
  130. const userStore = useUserInfo()
  131. const { id: userId } = storeToRefs(userStore)
  132. const loading = ref(false)
  133. const employeeList = ref<Employee[]>([
  134. ])
  135. const showCreateDialog = ref(false)
  136. const showAddMemberDialog = ref(false)
  137. const showProductSeatDialog = ref(false)
  138. const selectedMemberIdsForDialog = ref<number[]>([])
  139. const currentPage = ref(1)
  140. const pageSize = ref(10)
  141. const pageSizes = [10, 20, 30, 50]
  142. const total = ref(0)
  143. // 企业数据占位,后续可通过接口填充
  144. const companyId = ref<number | string | null>(null)
  145. const companyName = ref('')
  146. const hasEnterprise = computed(() => {
  147. const id = companyId.value
  148. if (id === null || id === undefined) return false
  149. if (id === '' || id === 0) return false
  150. return true
  151. })
  152. const currentCompanyId: any = computed(() => companyId.value)
  153. const currentEnterpriseName = computed(() => {
  154. if (!hasEnterprise.value) return '我的企业'
  155. if (companyName.value) return companyName.value
  156. return `企业 (ID: ${companyId.value})`
  157. })
  158. const currentUserId = computed(() => userId.value || null)
  159. // 请求锁,防止重复请求
  160. const isLoadingList = ref(false)
  161. // 获取企业列表及员工
  162. const loadEnterpriseList = async () => {
  163. if (!hasEnterprise.value) {
  164. loading.value = false
  165. employeeList.value = []
  166. total.value = 0
  167. return
  168. }
  169. // 如果正在加载,直接返回,避免重复请求
  170. if (isLoadingList.value) {
  171. return
  172. }
  173. isLoadingList.value = true
  174. loading.value = true
  175. try {
  176. // 获取企业成员列表
  177. const membersRes = await members({
  178. company_id: companyId.value,
  179. page: currentPage.value,
  180. pageSize: pageSize.value
  181. })
  182. employeeList.value = membersRes.data?.list || []
  183. total.value = membersRes.data?.total || 0
  184. } catch (error: any) {
  185. // 如果是取消的请求(axios 自动取消重复请求),不显示错误
  186. // axios 取消的请求通常会有 message 属性包含 'cancel' 或 'Cancel'
  187. if (error?.message && (error.message.includes('cancel') || error.message.includes('Cancel'))) {
  188. return
  189. }
  190. ElMessage.error('获取成员列表失败')
  191. employeeList.value = []
  192. total.value = 0
  193. } finally {
  194. loading.value = false
  195. isLoadingList.value = false
  196. }
  197. }
  198. const handlePageSizeChange = (val: number) => {
  199. pageSize.value = val
  200. currentPage.value = 1
  201. loadEnterpriseList()
  202. }
  203. const handleCurrentPageChange = (val: number) => {
  204. currentPage.value = val
  205. loadEnterpriseList()
  206. }
  207. const handleCreateSuccess = async (data?: any) => {
  208. // 创建成功后刷新企业信息
  209. if (data?.id) {
  210. currentPage.value = 1
  211. companyId.value = data.id
  212. companyName.value = data.name || ''
  213. // companyId 变化会触发 watch,自动加载列表,不需要手动调用
  214. } else {
  215. // 如果没有返回数据,重新检查企业信息
  216. await handleCheckCompany()
  217. }
  218. }
  219. const handleDialogClose = () => {
  220. showCreateDialog.value = false
  221. }
  222. // 创建企业
  223. const openCreateDialog = () => {
  224. console.log('openCreateDialog called, showCreateDialog before:', showCreateDialog.value)
  225. showCreateDialog.value = true
  226. console.log('showCreateDialog after:', showCreateDialog.value)
  227. }
  228. const handleDeleteMember = async (member: Employee) => {
  229. if (!member?.id) {
  230. ElMessage.error('成员信息不完整')
  231. return
  232. }
  233. if (!member.can_operate) {
  234. ElMessage.warning('您没有权限操作此成员')
  235. return
  236. }
  237. try {
  238. await ElMessageBox.confirm(`确定要删除成员 "${member.name || member.email}" 吗?`, '提示', {
  239. confirmButtonText: '确定',
  240. cancelButtonText: '取消',
  241. type: 'warning'
  242. })
  243. await removeMember({
  244. member_id: member.id,
  245. company_id: companyId.value
  246. })
  247. ElMessage.success('删除成员成功')
  248. loadEnterpriseList()
  249. } catch (error: any) {
  250. ElMessage.error('删除成员失败')
  251. }
  252. }
  253. // 检查是否有企业
  254. const handleCheckCompany = async () => {
  255. try {
  256. const res = await checkCompany()
  257. if (res.code === 1 && res.data?.has_company) {
  258. // 如果已有企业,获取企业信息
  259. const companiesRes = await myCompanies({})
  260. const company = companiesRes.data
  261. currentPage.value = 1
  262. companyId.value = company.id
  263. companyName.value = company.name || ''
  264. // companyId 变化会触发 watch,自动加载列表,不需要手动调用
  265. } else {
  266. // 没有企业,清空数据
  267. companyId.value = null
  268. companyName.value = ''
  269. }
  270. } catch (error) {
  271. console.error('检查企业失败:', error)
  272. ElMessage.error('检查企业状态失败')
  273. companyId.value = null
  274. companyName.value = ''
  275. }
  276. }
  277. // 选择分配员工
  278. const handleOpenProductSeatDialog = () => {
  279. // 直接打开分配弹窗,不再依赖表格选中
  280. selectedMemberIdsForDialog.value = []
  281. showProductSeatDialog.value = true
  282. }
  283. const handleProductSeatSuccess = (data: ProductSeatResult) => {
  284. loadEnterpriseList()
  285. const count = data?.memberIds?.length || 0
  286. ElMessage.success(
  287. data.mode === 'assign'
  288. ? `成功为 ${count} 位员工分配产品席位`
  289. : `成功取消 ${count} 位员工的产品席位`
  290. )
  291. }
  292. const handleProductSeatDialogClose = () => {
  293. showProductSeatDialog.value = false
  294. selectedMemberIdsForDialog.value = []
  295. }
  296. type TagType = 'success' | 'warning' | 'info' | 'danger' | 'primary'
  297. const getStatusType = (status: number): TagType => {
  298. const statusMap: Record<number, TagType> = {
  299. 1: 'success',
  300. 2: 'danger'
  301. }
  302. return statusMap[status] || 'info'
  303. }
  304. watch(
  305. companyId,
  306. (newVal, oldVal) => {
  307. // 只在 companyId 从无到有或发生变化时加载
  308. // 避免初始化时 immediate 触发(因为初始值为 null)
  309. if (newVal && newVal !== oldVal) {
  310. loadEnterpriseList()
  311. } else if (!newVal && oldVal) {
  312. // 如果企业被清空,清空列表
  313. employeeList.value = []
  314. total.value = 0
  315. }
  316. }
  317. )
  318. onMounted(() => {
  319. handleCheckCompany()
  320. })
  321. </script>
  322. <style scoped lang="scss">
  323. .me-enterprise {
  324. width: 100%;
  325. .enterprise-header {
  326. margin-bottom: 20px;
  327. padding-bottom: 15px;
  328. border-bottom: 1px solid #e4e7ed;
  329. .enterprise-title {
  330. margin: 0;
  331. font-size: 20px;
  332. font-weight: 500;
  333. color: #303133;
  334. }
  335. }
  336. .enterprise-content {
  337. padding: 20px 0;
  338. color: #606266;
  339. }
  340. .empty-state {
  341. display: flex;
  342. flex-direction: column;
  343. align-items: center;
  344. justify-content: center;
  345. padding: 60px 20px;
  346. text-align: center;
  347. .empty-icon {
  348. font-size: 64px;
  349. color: #c0c4cc;
  350. margin-bottom: 20px;
  351. .empty-icon__icon {
  352. font-size: 64px;
  353. }
  354. }
  355. .empty-text {
  356. margin: 0 0 30px 0;
  357. font-size: 16px;
  358. color: #909399;
  359. }
  360. }
  361. .loading-state {
  362. display: flex;
  363. flex-direction: column;
  364. align-items: center;
  365. justify-content: center;
  366. padding: 60px 20px;
  367. text-align: center;
  368. .loading-icon {
  369. font-size: 32px;
  370. color: #409eff;
  371. margin-bottom: 16px;
  372. animation: rotating 2s linear infinite;
  373. }
  374. p {
  375. margin: 0;
  376. font-size: 14px;
  377. color: #909399;
  378. }
  379. }
  380. .enterprise-list {
  381. .list-header {
  382. margin-bottom: 20px;
  383. padding: 20px 24px;
  384. background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
  385. border-radius: 12px;
  386. border: 1px solid #e4e7ed;
  387. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
  388. display: flex;
  389. justify-content: space-between;
  390. align-items: flex-start;
  391. gap: 20px;
  392. .enterprise-info {
  393. display: flex;
  394. flex-direction: column;
  395. gap: 12px;
  396. flex: 1;
  397. .enterprise-name {
  398. display: flex;
  399. align-items: center;
  400. gap: 8px;
  401. .enterprise-name-icon {
  402. font-size: 20px;
  403. color: #409eff;
  404. }
  405. .name-text {
  406. font-size: 18px;
  407. font-weight: 600;
  408. color: #303133;
  409. }
  410. }
  411. .hint-text {
  412. margin: 0;
  413. font-size: 13px;
  414. color: #909399;
  415. }
  416. }
  417. .header-actions {
  418. flex-shrink: 0;
  419. }
  420. }
  421. .table-section {
  422. margin-bottom: 20px;
  423. background: #fff;
  424. border-radius: 12px;
  425. overflow: hidden;
  426. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
  427. ::v-deep .el-table {
  428. border: none;
  429. &::before {
  430. display: none;
  431. }
  432. .el-table__header-wrapper {
  433. .el-table__header {
  434. th {
  435. border-bottom: 2px solid #f0f2f5;
  436. border-right: none;
  437. }
  438. }
  439. }
  440. .el-table__body-wrapper {
  441. .el-table__body {
  442. tr {
  443. td {
  444. border-bottom: 1px solid #f5f7fa;
  445. border-right: none;
  446. }
  447. &:hover {
  448. background-color: #fafbfc;
  449. }
  450. &:last-child {
  451. td {
  452. border-bottom: none;
  453. }
  454. }
  455. }
  456. }
  457. }
  458. }
  459. }
  460. .batch-actions {
  461. display: flex;
  462. justify-content: space-between;
  463. align-items: center;
  464. padding: 16px 20px;
  465. background: linear-gradient(135deg, #ecf5ff 0%, #e1f3ff 100%);
  466. border-radius: 12px;
  467. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
  468. margin-bottom: 20px;
  469. border: 1px solid #b3d8ff;
  470. .batch-info {
  471. font-size: 14px;
  472. color: #606266;
  473. strong {
  474. color: #409eff;
  475. font-weight: 600;
  476. }
  477. }
  478. .batch-buttons {
  479. display: flex;
  480. gap: 12px;
  481. }
  482. }
  483. .pagination-section {
  484. display: flex;
  485. justify-content: flex-end;
  486. align-items: center;
  487. padding: 16px 20px;
  488. background: #fff;
  489. border-radius: 12px;
  490. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
  491. margin-top: 20px;
  492. ::v-deep .el-pagination {
  493. .el-pagination__total {
  494. color: #606266;
  495. font-size: 14px;
  496. font-weight: 500;
  497. }
  498. }
  499. }
  500. }
  501. }
  502. @keyframes rotating {
  503. 0% {
  504. transform: rotate(0deg);
  505. }
  506. 100% {
  507. transform: rotate(360deg);
  508. }
  509. }
  510. @media (max-width: 768px) {
  511. .me-enterprise {
  512. .enterprise-list {
  513. .list-header {
  514. flex-direction: column;
  515. align-items: flex-start;
  516. gap: 12px;
  517. .hint-text {
  518. width: 100%;
  519. }
  520. .header-actions {
  521. width: 100%;
  522. .el-button {
  523. width: 100%;
  524. }
  525. }
  526. }
  527. .table-section {
  528. overflow-x: auto;
  529. }
  530. }
  531. }
  532. }
  533. </style>