index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. <template>
  2. <div class="page-container">
  3. <!-- 搜索区域 -->
  4. <el-card class="search-card">
  5. <el-form :model="queryForm" inline>
  6. <el-form-item label="手机号">
  7. <el-input v-model="queryForm.phone" placeholder="请输入手机号" clearable style="width:180px" />
  8. </el-form-item>
  9. <el-form-item label="昵称">
  10. <el-input v-model="queryForm.nickname" placeholder="请输入昵称" clearable style="width:180px" />
  11. </el-form-item>
  12. <el-form-item label="用户身份">
  13. <el-select v-model="queryForm.userType" placeholder="全部" clearable style="width:120px">
  14. <el-option
  15. v-for="item in userTypeOptions"
  16. :key="item.code"
  17. :label="item.name"
  18. :value="item.code"
  19. />
  20. </el-select>
  21. </el-form-item>
  22. <el-form-item label="用户类别">
  23. <el-select
  24. v-model="queryForm.userCategoryId"
  25. placeholder="全部"
  26. clearable
  27. :disabled="queryForm.userType && queryForm.userType !== 1"
  28. style="width:140px"
  29. >
  30. <el-option
  31. v-for="item in userCategoryOptions"
  32. :key="item.id"
  33. :label="item.name"
  34. :value="item.id"
  35. />
  36. </el-select>
  37. </el-form-item>
  38. <el-form-item label="状态">
  39. <el-select v-model="queryForm.status" placeholder="全部" clearable style="width:100px">
  40. <el-option label="正常" :value="1" />
  41. <el-option label="禁用" :value="0" />
  42. </el-select>
  43. </el-form-item>
  44. <el-form-item>
  45. <el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
  46. <el-button :icon="Refresh" @click="handleReset">重置</el-button>
  47. </el-form-item>
  48. </el-form>
  49. </el-card>
  50. <!-- 表格区域 -->
  51. <el-card class="table-card">
  52. <template #header>
  53. <div class="card-header">
  54. <span class="title">账号管理列表</span>
  55. <el-button type="primary" :icon="Plus" @click="handleAdd">新增用户</el-button>
  56. </div>
  57. </template>
  58. <el-table :data="tableData" v-loading="loading" border stripe>
  59. <el-table-column type="index" label="#" width="55" align="center" />
  60. <el-table-column prop="phone" label="手机号" width="130" />
  61. <el-table-column prop="nickname" label="昵称" />
  62. <el-table-column prop="userType" label="用户身份" width="100" align="center">
  63. <template #default="{ row }">
  64. <el-tag
  65. :type="userTypeTagType(row.userType)"
  66. size="small"
  67. >
  68. {{ userTypeText(row.userType) }}
  69. </el-tag>
  70. </template>
  71. </el-table-column>
  72. <el-table-column prop="userCategoryId" label="用户类别" width="120" align="center">
  73. <template #default="{ row }">
  74. <span v-if="row.userType === 1">{{ userCategoryText(row.userCategoryId) }}</span>
  75. <span v-else>-</span>
  76. </template>
  77. </el-table-column>
  78. <el-table-column prop="status" label="状态" width="80" align="center">
  79. <template #default="{ row }">
  80. <el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
  81. {{ row.status === 1 ? '正常' : '禁用' }}
  82. </el-tag>
  83. </template>
  84. </el-table-column>
  85. <el-table-column prop="s" label="绑定设备" width="170" />
  86. <el-table-column prop="s" label="子用户" width="170" />
  87. <el-table-column prop="createTime" label="注册时间" width="170" :formatter="dateTimeFormatter" />
  88. <el-table-column label="操作" width="240" align="center" fixed="right">
  89. <template #default="{ row }">
  90. <el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
  91. <el-button
  92. size="small"
  93. :type="row.status === 1 ? 'warning' : 'success'"
  94. link
  95. @click="handleToggleStatus(row)"
  96. >
  97. {{ row.status === 1 ? '禁用' : '启用' }}
  98. </el-button>
  99. <!-- <el-button size="small" type="info" link @click="handleViewProfiles(row)">子用户</el-button> -->
  100. <el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
  101. </template>
  102. </el-table-column>
  103. </el-table>
  104. <!-- 分页 -->
  105. <el-pagination
  106. v-model:current-page="queryForm.pageNum"
  107. v-model:page-size="queryForm.pageSize"
  108. :page-sizes="[10, 20, 50]"
  109. :total="total"
  110. :hide-on-single-page="false"
  111. layout="total, sizes, prev, pager, next, jumper"
  112. class="pagination"
  113. @size-change="loadData"
  114. @current-change="loadData"
  115. />
  116. </el-card>
  117. <!-- 新增/编辑弹窗 -->
  118. <el-dialog
  119. v-model="dialogVisible"
  120. :title="dialogTitle"
  121. width="500px"
  122. destroy-on-close
  123. >
  124. <el-form :model="formData" :rules="formRules" ref="formRef" label-width="90px">
  125. <el-form-item label="手机号" prop="phone">
  126. <el-input v-model="formData.phone" placeholder="请输入手机号" :disabled="!!formData.id" />
  127. </el-form-item>
  128. <el-form-item label="昵称" prop="nickname">
  129. <el-input v-model="formData.nickname" placeholder="请输入昵称" />
  130. </el-form-item>
  131. <el-form-item label="用户身份" prop="userType">
  132. <el-select v-model="formData.userType" placeholder="请选择" style="width:100%">
  133. <el-option
  134. v-for="item in userTypeOptions"
  135. :key="item.code"
  136. :label="item.name"
  137. :value="item.code"
  138. />
  139. </el-select>
  140. </el-form-item>
  141. <el-form-item v-if="formData.userType === 1" label="用户类别" prop="userCategoryId">
  142. <el-select v-model="formData.userCategoryId" placeholder="请选择用户类别" clearable style="width:100%">
  143. <el-option
  144. v-for="item in userCategoryOptions"
  145. :key="item.id"
  146. :label="item.name"
  147. :value="item.id"
  148. />
  149. </el-select>
  150. </el-form-item>
  151. <el-form-item label="状态" prop="status">
  152. <el-radio-group v-model="formData.status">
  153. <el-radio :label="1">正常</el-radio>
  154. <el-radio :label="0">禁用</el-radio>
  155. </el-radio-group>
  156. </el-form-item>
  157. </el-form>
  158. <template #footer>
  159. <el-button @click="dialogVisible = false">取消</el-button>
  160. <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
  161. </template>
  162. </el-dialog>
  163. </div>
  164. </template>
  165. <script setup>
  166. import { ref, reactive, onMounted, watch } from 'vue'
  167. import { useRouter } from 'vue-router'
  168. import { ElMessage, ElMessageBox } from 'element-plus'
  169. import { Search, Refresh, Plus } from '@element-plus/icons-vue'
  170. import {
  171. getAppUserPage,
  172. addAppUser,
  173. updateAppUser,
  174. deleteAppUser,
  175. updateAppUserStatus
  176. } from '@/api/user'
  177. import { getUserCategoryList } from '@/api/userCategory'
  178. import { dateTimeFormatter } from '@/utils/date'
  179. const router = useRouter()
  180. /** 用户身份固定选项(app_user.user_type 主分类) */
  181. const userTypeOptions = [
  182. { code: 1, name: '用户', tagType: 'primary' },
  183. { code: 2, name: '专家', tagType: 'warning' },
  184. { code: 3, name: '管理员', tagType: 'danger' }
  185. ]
  186. /** 用户类别选项(用户 user_type=1 的子分类) */
  187. const userCategoryOptions = ref([])
  188. /** 用户身份文本 */
  189. function userTypeText(type) {
  190. if (type == null) return '-'
  191. const item = userTypeOptions.find(c => c.code === type)
  192. return item ? item.name : '-'
  193. }
  194. /** 用户身份标签颜色 */
  195. function userTypeTagType(type) {
  196. if (type == null) return 'info'
  197. const item = userTypeOptions.find(c => c.code === type)
  198. return item?.tagType || 'primary'
  199. }
  200. function userCategoryText(categoryId) {
  201. if (!categoryId) return '-'
  202. const item = userCategoryOptions.value.find(c => c.id === categoryId)
  203. return item ? item.name : '-'
  204. }
  205. // 查询表单
  206. const queryForm = reactive({
  207. phone: '',
  208. nickname: '',
  209. userType: null,
  210. userCategoryId: null,
  211. status: null,
  212. pageNum: 1,
  213. pageSize: 10
  214. })
  215. const tableData = ref([])
  216. const total = ref(0)
  217. const loading = ref(false)
  218. // 弹窗
  219. const dialogVisible = ref(false)
  220. const dialogTitle = ref('')
  221. const submitLoading = ref(false)
  222. const formRef = ref(null)
  223. const formData = reactive({
  224. id: null,
  225. phone: '',
  226. nickname: '',
  227. userType: 1,
  228. userCategoryId: null,
  229. status: 1
  230. })
  231. const formRules = {
  232. phone: [
  233. { required: true, message: '请输入手机号', trigger: 'blur' },
  234. { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
  235. ],
  236. userType: [{ required: true, message: '请选择用户身份', trigger: 'change' }]
  237. }
  238. // 加载数据
  239. async function loadData() {
  240. loading.value = true
  241. try {
  242. const res = await getAppUserPage(queryForm)
  243. tableData.value = res.data?.records || []
  244. total.value = Number(res.data?.total) || 0
  245. } finally {
  246. loading.value = false
  247. }
  248. }
  249. function handleSearch() {
  250. queryForm.pageNum = 1
  251. loadData()
  252. }
  253. function handleReset() {
  254. queryForm.phone = ''
  255. queryForm.nickname = ''
  256. queryForm.userType = null
  257. queryForm.userCategoryId = null
  258. queryForm.status = null
  259. queryForm.pageNum = 1
  260. loadData()
  261. }
  262. // 新增
  263. function handleAdd() {
  264. Object.assign(formData, { id: null, phone: '', nickname: '', userType: 1, userCategoryId: null, status: 1 })
  265. dialogTitle.value = '新增账号'
  266. dialogVisible.value = true
  267. }
  268. // 编辑
  269. function handleEdit(row) {
  270. Object.assign(formData, {
  271. id: row.id,
  272. phone: row.phone,
  273. nickname: row.nickname,
  274. userType: row.userType,
  275. userCategoryId: row.userCategoryId || null,
  276. status: row.status
  277. })
  278. dialogTitle.value = '编辑账号'
  279. dialogVisible.value = true
  280. }
  281. // 提交
  282. async function handleSubmit() {
  283. await formRef.value.validate()
  284. submitLoading.value = true
  285. try {
  286. if (formData.id) {
  287. await updateAppUser({ ...formData })
  288. } else {
  289. await addAppUser({ ...formData })
  290. }
  291. ElMessage.success('操作成功')
  292. dialogVisible.value = false
  293. loadData()
  294. } finally {
  295. submitLoading.value = false
  296. }
  297. }
  298. // 禁用/启用
  299. async function handleToggleStatus(row) {
  300. const newStatus = row.status === 1 ? 0 : 1
  301. const action = newStatus === 0 ? '禁用' : '启用'
  302. await ElMessageBox.confirm(`确认${action}该用户吗?`, '提示', { type: 'warning' })
  303. await updateAppUserStatus(row.id, newStatus)
  304. ElMessage.success(`已${action}`)
  305. loadData()
  306. }
  307. // 查看子用户
  308. function handleViewProfiles(row) {
  309. router.push({ path: '/user/profile', query: { userId: row.id } })
  310. }
  311. // 删除
  312. async function handleDelete(row) {
  313. await ElMessageBox.confirm('确认删除该用户吗?删除后不可恢复!', '提示', { type: 'warning' })
  314. await deleteAppUser(row.id)
  315. ElMessage.success('删除成功')
  316. loadData()
  317. }
  318. async function loadUserCategories() {
  319. try {
  320. const res = await getUserCategoryList()
  321. userCategoryOptions.value = res.data || []
  322. } catch (e) {
  323. userCategoryOptions.value = []
  324. }
  325. }
  326. watch(
  327. () => formData.userType,
  328. userType => {
  329. if (userType !== 1) {
  330. formData.userCategoryId = null
  331. }
  332. }
  333. )
  334. watch(
  335. () => queryForm.userType,
  336. userType => {
  337. if (userType && userType !== 1) {
  338. queryForm.userCategoryId = null
  339. }
  340. }
  341. )
  342. onMounted(() => {
  343. loadUserCategories()
  344. loadData()
  345. })
  346. </script>
  347. <style lang="scss" scoped>
  348. .page-container {
  349. display: flex;
  350. flex-direction: column;
  351. gap: 16px;
  352. }
  353. .search-card {
  354. :deep(.el-card__body) {
  355. padding: 16px 20px 4px;
  356. }
  357. }
  358. .table-card {
  359. .card-header {
  360. display: flex;
  361. align-items: center;
  362. justify-content: space-between;
  363. .title {
  364. font-weight: 600;
  365. font-size: 15px;
  366. }
  367. }
  368. }
  369. .pagination {
  370. margin-top: 16px;
  371. display: flex;
  372. justify-content: flex-end;
  373. }
  374. </style>