From 073605b6d62363dd7d48f5ca047a1d401418574a Mon Sep 17 00:00:00 2001
From: zzw <228544128@qq.com>
Date: 星期日, 04 十月 2026 17:34:06 +0800
Subject: [PATCH] docs: record 2026-10-04 integration commit

---
 emergency-repair-web/src/views/system/UserManage.vue |  253 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 253 insertions(+), 0 deletions(-)

diff --git a/emergency-repair-web/src/views/system/UserManage.vue b/emergency-repair-web/src/views/system/UserManage.vue
new file mode 100644
index 0000000..2e3c9a5
--- /dev/null
+++ b/emergency-repair-web/src/views/system/UserManage.vue
@@ -0,0 +1,253 @@
+<template>
+  <div class="page">
+    <div class="page-title">
+      <div><h2>鐢ㄦ埛绠$悊</h2><p>宸︿晶鏌ヨ鐢ㄦ埛锛屽彸渚ц缃鐢ㄦ埛鎷ユ湁鐨勮鑹诧紝涓�涓敤鎴峰彲璁剧疆澶氫釜瑙掕壊</p></div>
+    </div>
+    <div class="user-layout">
+      <section class="user-left">
+        <div class="toolbar">
+          <el-button type="primary" plain icon="el-icon-s-check" :disabled="!selectedRows.length" v-permission="'system:users:assign'" @click="openBatchDialog">鎵归噺璁剧疆瑙掕壊</el-button>
+          <span v-if="selectedRows.length" class="selected-count">宸查�� {{ selectedRows.length }} 浜�</span>
+          <el-input v-model="filters.keyword" clearable placeholder="鐢ㄦ埛鍚� / 璐︽埛鍚�" style="width:190px" @keyup.enter.native="search"></el-input>
+          <el-select v-model="filters.deptId" clearable filterable placeholder="鎵�灞為儴闂�" style="width:200px">
+            <el-option v-for="item in depts" :key="item.id" :label="item.name" :value="item.id"></el-option>
+          </el-select>
+          <el-select v-model="filters.roleId" clearable placeholder="瑙掕壊" style="width:170px">
+            <el-option v-for="item in roles" :key="item.id" :label="item.name" :value="item.id"></el-option>
+          </el-select>
+          <el-button type="primary" icon="el-icon-search" v-permission="'system:users:query'" @click="search">鏌ヨ</el-button>
+          <el-button icon="el-icon-refresh-left" @click="reset">閲嶇疆</el-button>
+        </div>
+        <el-table ref="userTable" row-key="userId"
+          v-loading="loading"
+          :data="rows"
+          border
+          stripe
+          highlight-current-row
+          height="calc(100vh - 250px)"
+          @selection-change="selectedRows = $event"
+          @current-change="selectUser"
+        >
+          <el-table-column type="selection" width="46" align="center"></el-table-column>
+          <el-table-column prop="nickName" label="鐢ㄦ埛鍚�" min-width="110"></el-table-column>
+          <el-table-column prop="userName" label="璐︽埛鍚�" min-width="140"></el-table-column>
+          <el-table-column prop="deptName" label="鎵�灞為儴闂�" min-width="180" show-overflow-tooltip></el-table-column>
+          <el-table-column label="瑙掕壊鏉冮檺" min-width="220">
+            <template slot-scope="{ row }">
+              <el-tag v-for="role in row.roles" :key="role.roleId" size="mini" class="role-tag">{{ role.roleName }}</el-tag>
+              <span v-if="!row.roles || !row.roles.length" class="muted">鏈垎閰�</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="phoneNumber" label="鎵嬫満鍙�" width="130"></el-table-column>
+          <el-table-column label="鐘舵��" width="80" align="center">
+            <template slot-scope="{ row }">
+              <el-tag size="mini" :type="isDisabled(row) ? 'info' : 'success'">{{ isDisabled(row) ? '鍋滅敤' : '姝e父' }}</el-tag>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-pagination
+          class="pager"
+          background
+          layout="total, sizes, prev, pager, next"
+          :total="total"
+          :current-page="current"
+          :page-size="size"
+          :page-sizes="[20, 50, 100]"
+          @current-change="handleCurrentChange"
+          @size-change="handleSizeChange"
+        ></el-pagination>
+      </section>
+      <section class="user-right">
+        <div class="panel-head">
+          <h3>瑙掕壊鎺堟潈</h3>
+          <span v-if="selected" class="selected-user">{{ selected.nickName || selected.userName }}锛坽{ selected.userName }}锛�</span>
+        </div>
+        <div v-if="!selected" class="empty-tip">璇烽�夋嫨宸︿晶鐢ㄦ埛</div>
+        <template v-else>
+          <el-checkbox-group v-model="checkedRoleIds" class="role-list">
+            <el-checkbox v-for="item in roles" :key="item.id" :label="item.id" class="role-item">
+              <span class="role-name">{{ item.name }}</span>
+              <span class="role-code">{{ item.code }}</span>
+            </el-checkbox>
+          </el-checkbox-group>
+          <el-button
+            type="primary"
+            icon="el-icon-check"
+            class="save-button"
+            v-permission="'system:users:assign'"
+            :loading="saving"
+            @click="saveRoles"
+          >淇濆瓨鎺堟潈</el-button>
+        </template>
+      </section>
+    </div>
+      <el-dialog title="鎵归噺璁剧疆瑙掕壊" :visible.sync="batchVisible" width="520px" append-to-body :close-on-click-modal="false">
+        <div class="batch-summary">宸查�夋嫨 {{ selectedRows.length }} 涓敤鎴�</div>
+        <el-radio-group v-model="batchMode" class="batch-mode">
+          <el-radio label="append">杩藉姞瑙掕壊</el-radio>
+          <el-radio label="replace">瑕嗙洊瑙掕壊</el-radio>
+        </el-radio-group>
+        <el-alert :title="batchMode === 'append' ? '淇濈暀鎵�閫夌敤鎴峰師鏈夎鑹诧紝骞惰拷鍔犲嬀閫夎鑹�' : '浣跨敤鍕鹃�夎鑹茶鐩栨墍閫夌敤鎴风殑鍘熸湁瑙掕壊'" type="warning" :closable="false" show-icon></el-alert>
+        <el-checkbox-group v-model="batchRoleIds" class="batch-role-list">
+          <el-checkbox v-for="item in roles" :key="item.id" :label="item.id" class="role-item">
+            <span class="role-name">{{ item.name }}</span>
+            <span class="role-code">{{ item.code }}</span>
+          </el-checkbox>
+        </el-checkbox-group>
+        <span slot="footer">
+          <el-button @click="batchVisible = false">鍙栨秷</el-button>
+          <el-button type="primary" :loading="batchSaving" v-permission="'system:users:assign'" @click="saveBatchRoles">淇濆瓨</el-button>
+        </span>
+      </el-dialog>
+  </div>
+</template>
+<script>
+import api from '@/api'
+export default {
+  name: 'UserManage',
+  data() {
+    return {
+      rows: [],
+      total: 0,
+      current: 1,
+      size: 20,
+      loading: false,
+      saving: false,
+      batchSaving: false,
+      batchVisible: false,
+      batchRoleIds: [],
+      batchMode: 'append',
+      selectedRows: [],
+      depts: [],
+      roles: [],
+      selected: null,
+      checkedRoleIds: [],
+      filters: { keyword: '', deptId: null, roleId: null }
+    }
+  },
+  created() {
+    this.loadRoles()
+    this.loadDepts()
+    this.load()
+  },
+  methods: {
+    load() {
+      this.clearSelection()
+      this.loading = true
+      api.get('/system/users', {
+        params: {
+          keyword: this.filters.keyword || undefined,
+          deptId: this.filters.deptId || undefined,
+          roleId: this.filters.roleId || undefined,
+          current: this.current,
+          size: this.size
+        }
+      }).then(res => {
+        const data = res.data || {}
+        this.rows = data.records || []
+        this.total = data.total || 0
+        if (this.selected) {
+          const matched = this.rows.find(item => item.userId === this.selected.userId)
+          this.selected = matched || null
+          if (!matched) this.checkedRoleIds = []
+        }
+      }).finally(() => { this.loading = false })
+    },
+    loadRoles() {
+      api.get('/system/roles').then(res => { this.roles = res.data || [] })
+    },
+    loadDepts() {
+      api.get('/system/depts').then(res => { this.depts = res.data || [] })
+    },
+    search() {
+      this.current = 1
+      this.load()
+    },
+    reset() {
+      this.filters = { keyword: '', deptId: null, roleId: null }
+      this.search()
+    },
+    handleCurrentChange(page) {
+      this.current = page
+      this.load()
+    },
+    handleSizeChange(size) {
+      this.size = size
+      this.current = 1
+      this.load()
+    },
+    clearSelection() {
+      this.selectedRows = []
+      this.$nextTick(() => { if (this.$refs.userTable) this.$refs.userTable.clearSelection() })
+    },
+    openBatchDialog() {
+      if (!this.selectedRows.length) return
+      this.batchRoleIds = []
+      this.batchMode = 'append'
+      this.batchVisible = true
+    },
+    saveBatchRoles() {
+      if (!this.selectedRows.length) return
+      const userIds = this.selectedRows.map(item => item.userId)
+      const roleIds = this.batchRoleIds.slice()
+      if (this.batchMode === 'append' && !roleIds.length) {
+        this.$message.warning('璇烽�夋嫨瑕佽拷鍔犵殑瑙掕壊')
+        return
+      }
+      const message = this.batchMode === 'append'
+        ? '灏嗗悜 ' + userIds.length + ' 涓敤鎴疯拷鍔犳墍閫夎鑹诧紝鏄惁缁х画锛�'
+        : (roleIds.length ? '灏嗕娇鐢ㄦ墍閫夎鑹茶鐩� ' + userIds.length + ' 涓敤鎴风殑鐜版湁瑙掕壊锛屾槸鍚︾户缁紵' : '鏈�夋嫨瑙掕壊锛屽皢娓呯┖ ' + userIds.length + ' 涓敤鎴风殑鍏ㄩ儴瑙掕壊锛屾槸鍚︾户缁紵')
+      this.$confirm(message, '鎵归噺璁剧疆瑙掕壊', { type: 'warning' }).then(() => {
+        this.batchSaving = true
+        api.put('/system/users/roles/batch', { userIds, roleIds, mode: this.batchMode }).then(() => {
+          this.$message.success('鎵归噺瑙掕壊璁剧疆鎴愬姛')
+          this.batchVisible = false
+          this.load()
+          if (this.selected) this.selectUser(this.selected)
+        }).finally(() => { this.batchSaving = false })
+      }).catch(() => {})
+    },
+    selectUser(row) {
+      this.selected = row || null
+      this.checkedRoleIds = []
+      if (!row) return
+      api.get('/system/users/' + row.userId + '/roles').then(res => {
+        this.checkedRoleIds = res.data || []
+      })
+    },
+    saveRoles() {
+      if (!this.selected) return
+      this.saving = true
+      api.put('/system/users/' + this.selected.userId + '/roles', { roleIds: this.checkedRoleIds }).then(() => {
+        this.$message.success('瑙掕壊鎺堟潈宸蹭繚瀛�')
+        this.load()
+      }).finally(() => { this.saving = false })
+    },
+    isDisabled(row) {
+      return row.status === '1'
+    }
+  }
+}
+</script>
+<style scoped>
+.user-layout { display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 12px; }
+.user-left, .user-right { min-width: 0; padding: 14px; border: 1px solid #dce4ec; border-radius: 4px; background: #fff; }
+.user-right { min-height: calc(100vh - 190px); }
+.toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
+.selected-count { align-self: center; color: #5c6f82; font-size: 12px; }
+.batch-summary { margin-bottom: 10px; color: #31465d; font-size: 14px; }
+.batch-role-list { display: grid; grid-template-columns: 1fr 1fr; gap: 2px 12px; max-height: 320px; margin-top: 14px; overflow: auto; }
+.pager { margin-top: 12px; text-align: right; }
+.panel-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid #edf1f5; }
+.panel-head h3 { margin: 0; color: #31465d; font-size: 15px; }
+.selected-user { color: #5c6f82; font-size: 13px; }
+.empty-tip { padding: 60px 0; color: #93a2b1; text-align: center; }
+.role-list { display: flex; flex-direction: column; gap: 4px; max-height: calc(100vh - 320px); overflow: auto; }
+.role-item { display: flex; align-items: center; height: 34px; margin-right: 0; }
+.role-name { color: #31465d; }
+.role-code { margin-left: 8px; color: #93a2b1; font-size: 12px; }
+.save-button { width: 100%; margin-top: 16px; }
+.role-tag { margin: 2px 4px 2px 0; }
+.muted { color: #a3afbc; }
+@media (max-width: 1100px) { .user-layout { grid-template-columns: 1fr; } }
+</style>

--
Gitblit v1.9.1