VirtualTeamDetail.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. <template>
  2. <div class="virtual-team-detail admin-page">
  3. <el-card>
  4. <div slot="header">
  5. <el-button size="small" icon="el-icon-back" @click="goBack">返回</el-button>
  6. <span style="margin-left: 12px">团队成员管理 - {{ teamName }}</span>
  7. <el-button style="float: right" type="primary" size="small" @click="showAddDialog">+ 添加成员</el-button>
  8. </div>
  9. <div class="team-info" v-loading="infoLoading">
  10. <el-descriptions :column="3" border size="small">
  11. <el-descriptions-item label="团队名称">{{ teamInfo.name || '-' }}</el-descriptions-item>
  12. <el-descriptions-item label="描述">{{ teamInfo.description || '-' }}</el-descriptions-item>
  13. <el-descriptions-item label="利润率">{{ teamInfo.profitRate != null ? teamInfo.profitRate + '%' : '-' }}</el-descriptions-item>
  14. </el-descriptions>
  15. </div>
  16. </el-card>
  17. <el-card style="margin-top: 16px">
  18. <div slot="header">
  19. <span>成员列表</span>
  20. </div>
  21. <div style="overflow:auto;max-height:calc(100vh - 300px);">
  22. <el-table style="max-height:calc(100vh - 300px);" :data="members" stripe v-loading="membersLoading">
  23. <el-table-column prop="id" label="关系ID" width="80"></el-table-column>
  24. <el-table-column prop="userId" label="用户ID" width="100"></el-table-column>
  25. <el-table-column label="用户昵称" width="140">
  26. <template slot-scope="scope">
  27. <div style="display: flex; align-items: center; gap: 6px">
  28. <el-avatar v-if="scope.row.userAvatar" :size="24" :src="scope.row.userAvatar"></el-avatar>
  29. <span>{{ scope.row.userNickname || '-' }}</span>
  30. </div>
  31. </template>
  32. </el-table-column>
  33. <el-table-column prop="depth" label="层级" width="80"></el-table-column>
  34. <el-table-column label="上级" width="140">
  35. <template slot-scope="scope">
  36. {{ getParentName(scope.row) }}
  37. </template>
  38. </el-table-column>
  39. <el-table-column label="操作" width="200" fixed="right">
  40. <template slot-scope="{ row }">
  41. <el-button size="mini" type="primary" @click="showReassignDialog(row)">变更上级</el-button>
  42. <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
  43. <el-button size="mini">
  44. 更多<i class="el-icon-arrow-down el-icon--right"></i>
  45. </el-button>
  46. <el-dropdown-menu slot="dropdown">
  47. <el-dropdown-item command="remove" icon="el-icon-remove">移除</el-dropdown-item>
  48. </el-dropdown-menu>
  49. </el-dropdown>
  50. </template>
  51. </el-table-column>
  52. </el-table></div>
  53. </el-card>
  54. <el-dialog :visible.sync="addDialogVisible" title="添加成员" width="500px" :close-on-click-modal="false">
  55. <el-form :model="addForm" label-width="80px">
  56. <el-form-item label="用户">
  57. <el-select
  58. v-model="addForm.userId"
  59. filterable
  60. remote
  61. reserve-keyword
  62. placeholder="搜索用户(姓名/手机号)"
  63. :remote-method="searchUsers"
  64. :loading="userSearchLoading"
  65. style="width: 100%"
  66. >
  67. <el-option
  68. v-for="u in userSearchResults"
  69. :key="u.id"
  70. :label="(u.nickname || u.username || '') + ' (ID:' + u.id + ')'"
  71. :value="u.id"
  72. ></el-option>
  73. </el-select>
  74. </el-form-item>
  75. <el-form-item label="上级">
  76. <el-select v-model="addForm.parentId" placeholder="无上级(根节点)" clearable style="width: 100%">
  77. <el-option
  78. v-for="m in members"
  79. :key="m.id"
  80. :label="(m.userNickname || 'ID:' + m.userId) + ' (层级:' + m.depth + ')'"
  81. :value="m.id"
  82. ></el-option>
  83. </el-select>
  84. </el-form-item>
  85. </el-form>
  86. <div slot="footer">
  87. <el-button @click="addDialogVisible = false">取消</el-button>
  88. <el-button type="primary" :loading="addSubmitting" @click="handleAddMember">添加</el-button>
  89. </div>
  90. </el-dialog>
  91. <el-dialog :visible.sync="reassignDialogVisible" title="变更上级" width="500px" :close-on-click-modal="false">
  92. <el-form :model="reassignForm" label-width="80px">
  93. <el-form-item label="成员">
  94. <span>{{ reassignForm.memberName }}</span>
  95. </el-form-item>
  96. <el-form-item label="新上级">
  97. <el-select v-model="reassignForm.parentId" placeholder="无上级(根节点)" clearable style="width: 100%">
  98. <el-option
  99. v-for="m in reassignCandidates"
  100. :key="m.id"
  101. :label="(m.userNickname || 'ID:' + m.userId) + ' (层级:' + m.depth + ')'"
  102. :value="m.id"
  103. ></el-option>
  104. </el-select>
  105. </el-form-item>
  106. </el-form>
  107. <div slot="footer">
  108. <el-button @click="reassignDialogVisible = false">取消</el-button>
  109. <el-button type="primary" :loading="reassignSubmitting" @click="handleReassign">确认</el-button>
  110. </div>
  111. </el-dialog>
  112. </div>
  113. </template>
  114. <script>
  115. import {
  116. listVirtualTeams,
  117. getVirtualTeamMembers,
  118. addVirtualTeamMember,
  119. removeVirtualTeamMember,
  120. updateVirtualTeamMember,
  121. searchUsers
  122. } from '@/api/admin'
  123. export default {
  124. name: 'VirtualTeamDetail',
  125. props: {
  126. systemId: { type: [String, Number], required: true }
  127. },
  128. data() {
  129. return {
  130. teamInfo: {},
  131. infoLoading: false,
  132. members: [],
  133. membersLoading: false,
  134. addDialogVisible: false,
  135. addSubmitting: false,
  136. addForm: { userId: null, parentId: null },
  137. userSearchLoading: false,
  138. userSearchResults: [],
  139. reassignDialogVisible: false,
  140. reassignSubmitting: false,
  141. reassignForm: { id: null, parentId: null, memberName: '' }
  142. }
  143. },
  144. computed: {
  145. teamName() {
  146. return this.teamInfo.name || '加载中...'
  147. },
  148. reassignCandidates() {
  149. var self = this
  150. return this.members.filter(function(m) {
  151. return m.id !== self.reassignForm.id
  152. })
  153. }
  154. },
  155. created() {
  156. this.loadTeamInfo()
  157. this.loadMembers()
  158. },
  159. methods: {
  160. goBack() {
  161. this.$router.push('/virtual-teams')
  162. },
  163. getParentName(row) {
  164. if (!row.parentId) return '无(根节点)'
  165. var parent = this.members.find(function(m) { return m.id === row.parentId })
  166. if (parent) return parent.userNickname || ('ID:' + parent.userId)
  167. return 'ID:' + row.parentId
  168. },
  169. async loadTeamInfo() {
  170. this.infoLoading = true
  171. try {
  172. var res = await listVirtualTeams({})
  173. var list = res.data || []
  174. var id = parseInt(this.systemId)
  175. this.teamInfo = list.find(function(t) { return t.id === id }) || {}
  176. } catch (e) {
  177. console.error(e)
  178. } finally {
  179. this.infoLoading = false
  180. }
  181. },
  182. async loadMembers() {
  183. this.membersLoading = true
  184. try {
  185. var res = await getVirtualTeamMembers(this.systemId)
  186. this.members = res.data || []
  187. } catch (e) {
  188. console.error(e)
  189. } finally {
  190. this.membersLoading = false
  191. }
  192. },
  193. showAddDialog() {
  194. this.addForm = { userId: null, parentId: null }
  195. this.userSearchResults = []
  196. this.addDialogVisible = true
  197. },
  198. async searchUsers(query) {
  199. if (!query || query.length < 1) {
  200. this.userSearchResults = []
  201. return
  202. }
  203. this.userSearchLoading = true
  204. try {
  205. var res = await searchUsers({ keyword: query })
  206. this.userSearchResults = res.data || []
  207. } catch (e) {
  208. this.userSearchResults = []
  209. } finally {
  210. this.userSearchLoading = false
  211. }
  212. },
  213. async handleAddMember() {
  214. if (!this.addForm.userId) {
  215. this.$message.warning('请选择用户')
  216. return
  217. }
  218. this.addSubmitting = true
  219. try {
  220. await addVirtualTeamMember({
  221. systemId: this.systemId,
  222. userId: this.addForm.userId,
  223. parentId: this.addForm.parentId
  224. })
  225. this.$message.success('添加成功')
  226. this.addDialogVisible = false
  227. this.loadMembers()
  228. } catch (e) {
  229. this.$message.error('添加失败')
  230. } finally {
  231. this.addSubmitting = false
  232. }
  233. },
  234. async handleRemove(row) {
  235. var hasChildren = this.members.some(function(m) { return m.parentId === row.id })
  236. if (hasChildren) {
  237. this.$message.warning('该成员有下级成员,请先变更下级的上级再移除')
  238. return
  239. }
  240. try {
  241. await this.$confirm('确定移除成员"' + (row.userNickname || row.userId) + '"吗?', '提示', { type: 'warning' })
  242. await removeVirtualTeamMember(row.id)
  243. this.$message.success('移除成功')
  244. this.loadMembers()
  245. } catch (e) {
  246. if (e !== 'cancel') this.$message.error('移除失败')
  247. }
  248. },
  249. showReassignDialog(row) {
  250. this.reassignForm = {
  251. id: row.id,
  252. parentId: row.parentId,
  253. memberName: row.userNickname || ('ID:' + row.userId)
  254. }
  255. this.reassignDialogVisible = true
  256. },
  257. async handleReassign() {
  258. this.reassignSubmitting = true
  259. try {
  260. await updateVirtualTeamMember({
  261. id: this.reassignForm.id,
  262. parentId: this.reassignForm.parentId
  263. })
  264. this.$message.success('上级变更成功')
  265. this.reassignDialogVisible = false
  266. this.loadMembers()
  267. } catch (e) {
  268. this.$message.error('变更失败')
  269. } finally {
  270. this.reassignSubmitting = false
  271. }
  272. },
  273. handleActionCmd(row, cmd) {
  274. switch (cmd) {
  275. case 'remove':
  276. this.handleRemove(row)
  277. break
  278. }
  279. }
  280. }
  281. }
  282. </script>
  283. <style scoped>
  284. .virtual-team-detail { padding: 20px; }
  285. .team-info { margin-bottom: 0; }
  286. </style>