|
|
@@ -9,12 +9,10 @@
|
|
|
|
|
|
<section class="panel">
|
|
|
<div class="panel-top">
|
|
|
- <h2>
|
|
|
- <span class="title-ic" aria-hidden="true">
|
|
|
- <el-icon><Operation /></el-icon>
|
|
|
- </span>
|
|
|
- 用户列表
|
|
|
- </h2>
|
|
|
+ <div class="panel-title">
|
|
|
+ <el-icon class="title-ic"><UserFilled /></el-icon>
|
|
|
+ <span>用户列表</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<div class="filter-block">
|
|
|
@@ -70,7 +68,7 @@
|
|
|
新增用户
|
|
|
</el-button>
|
|
|
|
|
|
- <el-button class="btn-ghost" type="danger" :disabled="selectedRows.length === 0" @click="handleBatchDelete">
|
|
|
+ <el-button class="btn-ghost" :disabled="selectedRows.length === 0" @click="handleBatchDelete">
|
|
|
批量删除
|
|
|
</el-button>
|
|
|
|
|
|
@@ -81,6 +79,7 @@
|
|
|
:filename="userExportFilename"
|
|
|
sheetName="用户数据"
|
|
|
:columns="userExportColumns"
|
|
|
+ type="default"
|
|
|
>
|
|
|
批量导出
|
|
|
</ArtExcelExport>
|
|
|
@@ -91,13 +90,22 @@
|
|
|
<div class="list-body list-wrap">
|
|
|
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="id" @selection-change="handleSelectionChange">
|
|
|
<el-table-column type="selection" width="55" />
|
|
|
- <el-table-column label="头像" width="72" align="center">
|
|
|
+ <el-table-column
|
|
|
+ label="姓名"
|
|
|
+ min-width="170"
|
|
|
+ align="left"
|
|
|
+ header-align="center"
|
|
|
+ class-name="user-name-col"
|
|
|
+ header-class-name="user-name-col-header"
|
|
|
+ >
|
|
|
<template #default="{ row }">
|
|
|
- <el-avatar :size="36" :src="resolveUserAvatarUrl(row.avatarUrl, row.role)" />
|
|
|
+ <div class="user-name-cell">
|
|
|
+ <el-avatar :size="36" :src="resolveUserAvatarUrl(row.avatarUrl, row.role)" />
|
|
|
+ <span class="user-name-text">{{ row.realName || '-' }}</span>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="username" label="用户名" min-width="150" />
|
|
|
- <el-table-column prop="realName" label="姓名" min-width="120" />
|
|
|
<el-table-column prop="phone" label="手机号" min-width="130" />
|
|
|
<el-table-column prop="role" label="角色" min-width="100">
|
|
|
<template #default="{ row }">
|
|
|
@@ -142,95 +150,144 @@
|
|
|
<el-dialog
|
|
|
v-model="dialogVisible"
|
|
|
:title="dialogTitle"
|
|
|
- width="600px"
|
|
|
+ width="680px"
|
|
|
@close="handleDialogClose"
|
|
|
>
|
|
|
- <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
|
|
- <el-form-item label="用户名" prop="username" v-if="!isEdit">
|
|
|
- <el-input v-model="formData.username" placeholder="请输入用户名" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="密码" prop="password" v-if="!isEdit">
|
|
|
- <el-input v-model="formData.password" type="password" placeholder="请输入密码" show-password />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="姓名" prop="realName">
|
|
|
- <el-input v-model="formData.realName" placeholder="请输入姓名" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="手机号" prop="phone">
|
|
|
- <el-input v-model="formData.phone" placeholder="请输入手机号" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="角色" prop="role">
|
|
|
- <el-select v-model="formData.role" placeholder="请选择角色">
|
|
|
- <el-option label="运营管理员" value="admin" />
|
|
|
- <el-option label="供应商" value="supplier" />
|
|
|
- <el-option label="客户" value="client" />
|
|
|
- <el-option label="配送员" value="deliver" />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="所属区域" prop="regionId">
|
|
|
- <el-select
|
|
|
- v-model="formData.regionId"
|
|
|
- placeholder="请选择区域"
|
|
|
- @visible-change="(open) => open && loadRegions()"
|
|
|
- >
|
|
|
- <el-option v-for="region in regionList" :key="region.id" :label="region.name" :value="region.id" />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="头像" prop="avatarUrl">
|
|
|
- <div class="avatar-field">
|
|
|
- <el-avatar :size="56" :src="resolveUserAvatarUrl(formData.avatarUrl, formData.role)" />
|
|
|
- <div class="avatar-actions">
|
|
|
- <el-upload
|
|
|
- :show-file-list="false"
|
|
|
- accept="image/jpeg,image/png,image/gif,image/webp"
|
|
|
- :before-upload="beforeAvatarUpload"
|
|
|
- >
|
|
|
- <el-button type="primary" link>上传图片</el-button>
|
|
|
- </el-upload>
|
|
|
- <el-button
|
|
|
- v-if="formData.avatarUrl"
|
|
|
- type="danger"
|
|
|
- link
|
|
|
- @click="formData.avatarUrl = ''"
|
|
|
- >
|
|
|
- 清除
|
|
|
- </el-button>
|
|
|
+ <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" class="user-dialog-form">
|
|
|
+ <div class="form-grid-2">
|
|
|
+ <el-form-item label="用户名" prop="username" v-if="!isEdit">
|
|
|
+ <el-input v-model="formData.username" placeholder="请输入用户名" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="密码" prop="password" v-if="!isEdit">
|
|
|
+ <el-input v-model="formData.password" type="password" placeholder="请输入密码" show-password />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="姓名" prop="realName">
|
|
|
+ <el-input v-model="formData.realName" placeholder="请输入姓名" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="手机号" prop="phone">
|
|
|
+ <el-input v-model="formData.phone" placeholder="请输入手机号" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="角色" prop="role">
|
|
|
+ <el-select v-model="formData.role" placeholder="请选择角色">
|
|
|
+ <el-option label="运营管理员" value="admin" />
|
|
|
+ <el-option label="供应商" value="supplier" />
|
|
|
+ <el-option label="客户" value="client" />
|
|
|
+ <el-option label="配送员" value="deliver" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="所属区域" prop="regionId">
|
|
|
+ <el-select
|
|
|
+ v-model="formData.regionId"
|
|
|
+ placeholder="请选择区域"
|
|
|
+ @visible-change="(open) => open && loadRegions()"
|
|
|
+ >
|
|
|
+ <el-option v-for="region in regionList" :key="region.id" :label="region.name" :value="region.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="头像" prop="avatarUrl" class="form-col-span-2">
|
|
|
+ <div class="avatar-field">
|
|
|
+ <el-avatar :size="56" :src="resolveUserAvatarUrl(formData.avatarUrl, formData.role)" />
|
|
|
+ <div class="avatar-actions">
|
|
|
+ <el-upload
|
|
|
+ :show-file-list="false"
|
|
|
+ accept="image/jpeg,image/png,image/gif,image/webp"
|
|
|
+ :before-upload="beforeAvatarUpload"
|
|
|
+ >
|
|
|
+ <el-button type="primary" link>上传图片</el-button>
|
|
|
+ </el-upload>
|
|
|
+ <el-button
|
|
|
+ v-if="formData.avatarUrl"
|
|
|
+ type="danger"
|
|
|
+ link
|
|
|
+ @click="formData.avatarUrl = ''"
|
|
|
+ >
|
|
|
+ 清除
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div class="avatar-tip">未上传时显示叁强 logo</div>
|
|
|
</div>
|
|
|
- <div class="avatar-tip">未上传时显示叁强 logo</div>
|
|
|
- </div>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="状态" prop="isActive" v-if="isEdit">
|
|
|
- <el-switch v-model="formData.isActive" />
|
|
|
- </el-form-item>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="状态" prop="isActive" v-if="isEdit">
|
|
|
+ <el-switch v-model="formData.isActive" />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
- <el-button class="btn-ghost" @click="dialogVisible = false">取消</el-button>
|
|
|
- <el-button class="btn-main" type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
|
|
|
+ <el-button class="btn-ghost" @click="dialogVisible = false">
|
|
|
+ <el-icon><Close /></el-icon>
|
|
|
+ 取消
|
|
|
+ </el-button>
|
|
|
+ <el-button class="btn-main" type="primary" @click="handleSubmit" :loading="submitLoading">
|
|
|
+ <el-icon><Check /></el-icon>
|
|
|
+ 确定
|
|
|
+ </el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
<!-- 查看详情弹窗 -->
|
|
|
- <el-dialog v-model="viewVisible" title="用户详情" width="600px">
|
|
|
- <div class="view-avatar-row">
|
|
|
- <el-avatar :size="72" :src="resolveUserAvatarUrl(viewData.avatarUrl, viewData.role)" />
|
|
|
+ <el-dialog v-model="viewVisible" title="用户详情" width="680px" class="user-view-dialog">
|
|
|
+ <div class="user-view-body">
|
|
|
+ <div class="view-hero">
|
|
|
+ <el-avatar :size="72" :src="resolveUserAvatarUrl(viewData.avatarUrl, viewData.role)" class="view-hero-avatar" />
|
|
|
+ <div class="view-hero-main">
|
|
|
+ <h2>{{ viewData.realName || viewData.username || '用户详情' }}</h2>
|
|
|
+ <div class="view-hero-tags">
|
|
|
+ <el-tag :type="getRoleTagType(viewData.role)" size="small">
|
|
|
+ {{ getRoleText(viewData.role) }}
|
|
|
+ </el-tag>
|
|
|
+ <el-tag :type="viewData.isActive ? 'success' : 'info'" size="small">
|
|
|
+ {{ viewData.isActive ? '启用' : '禁用' }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <p class="view-hero-meta">用户名:{{ viewData.username || '—' }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="view-section">
|
|
|
+ <h3>基础信息</h3>
|
|
|
+ <dl class="view-kv-grid">
|
|
|
+ <div class="view-kv">
|
|
|
+ <dt>用户名</dt>
|
|
|
+ <dd>{{ viewData.username || '—' }}</dd>
|
|
|
+ </div>
|
|
|
+ <div class="view-kv">
|
|
|
+ <dt>姓名</dt>
|
|
|
+ <dd>{{ viewData.realName || '—' }}</dd>
|
|
|
+ </div>
|
|
|
+ <div class="view-kv">
|
|
|
+ <dt>手机号</dt>
|
|
|
+ <dd>{{ viewData.phone || '—' }}</dd>
|
|
|
+ </div>
|
|
|
+ <div class="view-kv">
|
|
|
+ <dt>角色</dt>
|
|
|
+ <dd>{{ getRoleText(viewData.role) || '—' }}</dd>
|
|
|
+ </div>
|
|
|
+ <div class="view-kv">
|
|
|
+ <dt>所属区域</dt>
|
|
|
+ <dd>{{ viewData.regionName || '—' }}</dd>
|
|
|
+ </div>
|
|
|
+ <div class="view-kv">
|
|
|
+ <dt>状态</dt>
|
|
|
+ <dd>
|
|
|
+ <el-switch :model-value="!!viewData.isActive" disabled />
|
|
|
+ </dd>
|
|
|
+ </div>
|
|
|
+ <div class="view-kv">
|
|
|
+ <dt>创建时间</dt>
|
|
|
+ <dd>{{ viewData.createAt || '—' }}</dd>
|
|
|
+ </div>
|
|
|
+ <div class="view-kv">
|
|
|
+ <dt>创建人</dt>
|
|
|
+ <dd>{{ viewData.createName || '—' }}</dd>
|
|
|
+ </div>
|
|
|
+ </dl>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <el-descriptions :column="2" border>
|
|
|
- <el-descriptions-item label="用户名">{{ viewData.username }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="姓名">{{ viewData.realName }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="手机号">{{ viewData.phone }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="角色">
|
|
|
- <el-tag :type="getRoleTagType(viewData.role)" size="small">
|
|
|
- {{ getRoleText(viewData.role) }}
|
|
|
- </el-tag>
|
|
|
- </el-descriptions-item>
|
|
|
- <el-descriptions-item label="所属区域">{{ viewData.regionName }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="状态">
|
|
|
- <el-tag :type="viewData.isActive ? 'success' : 'danger'" size="small">
|
|
|
- {{ viewData.isActive ? '启用' : '禁用' }}
|
|
|
- </el-tag>
|
|
|
- </el-descriptions-item>
|
|
|
- <el-descriptions-item label="创建时间">{{ viewData.createAt }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="创建人">{{ viewData.createName }}</el-descriptions-item>
|
|
|
- </el-descriptions>
|
|
|
+ <template #footer>
|
|
|
+ <el-button class="btn-ghost" @click="viewVisible = false">
|
|
|
+ <el-icon><Close /></el-icon>
|
|
|
+ 关闭
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
</el-dialog>
|
|
|
</section>
|
|
|
</div>
|
|
|
@@ -240,7 +297,7 @@
|
|
|
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
|
|
- import { Operation, Plus } from '@element-plus/icons-vue'
|
|
|
+ import { Check, Close, Plus, UserFilled } from '@element-plus/icons-vue'
|
|
|
import {
|
|
|
fetchUserList,
|
|
|
fetchCreateUser,
|
|
|
@@ -389,7 +446,7 @@
|
|
|
admin: 'primary',
|
|
|
supplier: 'warning',
|
|
|
client: 'success',
|
|
|
- deliver: 'info'
|
|
|
+ deliver: 'danger'
|
|
|
}
|
|
|
return typeMap[role] || 'info'
|
|
|
}
|
|
|
@@ -644,9 +701,159 @@
|
|
|
color: #909399;
|
|
|
}
|
|
|
|
|
|
-.view-avatar-row {
|
|
|
+/* 用户列表/详情头像去掉默认浅灰底 */
|
|
|
+:deep(.el-avatar) {
|
|
|
+ background-color: transparent !important;
|
|
|
+}
|
|
|
+
|
|
|
+.user-name-cell {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ max-width: 100%;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.user-name-text {
|
|
|
+ color: var(--mint-text);
|
|
|
+ font-weight: 600;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.user-name-col .cell) {
|
|
|
+ justify-content: flex-start !important;
|
|
|
+ text-align: left !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.user-name-col-header .cell) {
|
|
|
+ justify-content: center !important;
|
|
|
+ text-align: center !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(th.user-name-col-header) {
|
|
|
+ text-align: center !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(th.user-name-col-header > .cell) {
|
|
|
+ justify-content: center !important;
|
|
|
+ text-align: center !important;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.user-dialog-form :deep(.el-form-item__label) {
|
|
|
+ height: auto !important;
|
|
|
+ line-height: 1.25 !important;
|
|
|
+ padding: 0 0 6px !important;
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--mint-text-muted);
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.form-grid-2 {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
+ gap: 0 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-col-span-2 {
|
|
|
+ grid-column: 1 / -1;
|
|
|
+}
|
|
|
+
|
|
|
+.user-view-body {
|
|
|
+ max-height: 62vh;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding-right: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.view-hero {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 16px;
|
|
|
+ padding: 16px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ background: linear-gradient(135deg, var(--mint-primary-bg) 0%, var(--mint-surface) 55%);
|
|
|
+ border: 1px solid var(--mint-border);
|
|
|
+ border-radius: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.view-hero-avatar {
|
|
|
+ flex-shrink: 0;
|
|
|
+ border: 1px solid var(--mint-border);
|
|
|
+}
|
|
|
+
|
|
|
+.view-hero-main {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 220px;
|
|
|
+}
|
|
|
+
|
|
|
+.view-hero-main h2 {
|
|
|
+ margin: 0 0 8px;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 700;
|
|
|
+ letter-spacing: -0.02em;
|
|
|
+ color: var(--mint-text);
|
|
|
+}
|
|
|
+
|
|
|
+.view-hero-tags {
|
|
|
display: flex;
|
|
|
- justify-content: center;
|
|
|
- margin-bottom: 16px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.view-hero-meta {
|
|
|
+ margin: 10px 0 0;
|
|
|
+ font-size: 13px;
|
|
|
+ color: var(--mint-text-muted);
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.view-section {
|
|
|
+ padding: 4px 2px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.view-section h3 {
|
|
|
+ margin: 0 0 12px;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: var(--mint-text-muted);
|
|
|
+ letter-spacing: 0.06em;
|
|
|
+ text-transform: uppercase;
|
|
|
+}
|
|
|
+
|
|
|
+.view-kv-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
+ gap: 12px 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.view-kv {
|
|
|
+ margin: 0;
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 92px 1fr;
|
|
|
+ gap: 8px 12px;
|
|
|
+ align-items: start;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.view-kv dt {
|
|
|
+ margin: 0;
|
|
|
+ color: var(--mint-text-muted);
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.view-kv dd {
|
|
|
+ margin: 0;
|
|
|
+ color: var(--mint-text);
|
|
|
+ word-break: break-word;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 760px) {
|
|
|
+ .view-kv-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|