feat(F006-W17.4): UserListView 新建/修改/导出 完整化
- 新增 UserFormDialog.vue (新建+修改共用弹窗) 字段: 昵称/手机号/密码/头像/性别/企业认证/禁用标识/备注 新建: POST /user/user/save 修改: GET /user/user/getUserInfo 回填, POST /user/user/update - 新建按钮: 顶部 + 新建用户 → 打开弹窗(空表单) - 修改按钮: 操作列 修改 → 打开弹窗(回填) - 导出按钮: 顶部 导出 → 调 GET /user/user/export (Excel 流, 自动下载) 带当前搜索条件, 后端响应头 filename 自动识别
This commit is contained in:
parent
9664b3d954
commit
c69fa216f8
255
src/modules/user/UserFormDialog.vue
Normal file
255
src/modules/user/UserFormDialog.vue
Normal file
@ -0,0 +1,255 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 用户表单弹窗 (新建/修改 共用)
|
||||
*
|
||||
* 后端:
|
||||
* - 新建: POST /user/user/save (UserDTO body)
|
||||
* - 详情: GET /user/user/getUserInfo?id=...
|
||||
* - 修改: POST /user/user/update (UserFeignDTO body, 字段子集)
|
||||
*
|
||||
* 字段 (沿用老 /views/api/user.vue + UserDTO.java):
|
||||
* nickName 昵称 (必填)
|
||||
* mobile 手机号 (必填, 11 位)
|
||||
* password 密码 (新建必填, 修改可选-留空表示不修改)
|
||||
* header 头像 URL (选填, 暂不实现上传, 直接 URL 输入)
|
||||
* sex 性别 0男1女 (选填)
|
||||
* isEnterprise 是否企业认证 0否1是 (选填)
|
||||
* enabled 禁用标识 0正常1禁用 (选填)
|
||||
* remark 备注 (选填)
|
||||
*/
|
||||
import { ref, reactive, watch, computed, nextTick } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { httpGet, httpPost } from '@/utils/request'
|
||||
|
||||
interface UserForm {
|
||||
id: number | null
|
||||
nickName: string
|
||||
mobile: string
|
||||
password: string
|
||||
header: string
|
||||
sex: '' | '0' | '1'
|
||||
isEnterprise: '' | '0' | '1'
|
||||
enabled: '' | '0' | '1'
|
||||
remark: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
userId: number | string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', v: boolean): void
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const mode = computed<'create' | 'update'>(() => (props.userId ? 'update' : 'create'))
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
const form = reactive<UserForm>({
|
||||
id: null,
|
||||
nickName: '',
|
||||
mobile: '',
|
||||
password: '',
|
||||
header: '',
|
||||
sex: '',
|
||||
isEnterprise: '',
|
||||
enabled: '0',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
nickName: [{ required: true, message: '请输入用户昵称', trigger: 'blur' }],
|
||||
mobile: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{
|
||||
validator(_: any, value: string, cb: (err?: Error) => void) {
|
||||
if (mode.value === 'create' && !value) {
|
||||
return cb(new Error('新建用户必须输入密码'))
|
||||
}
|
||||
if (value && (value.length < 6 || value.length > 20)) {
|
||||
return cb(new Error('密码长度 6-20 位'))
|
||||
}
|
||||
cb()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (v) => {
|
||||
if (!v) return
|
||||
resetForm()
|
||||
if (props.userId) {
|
||||
await loadDetail()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function resetForm() {
|
||||
form.id = null
|
||||
form.nickName = ''
|
||||
form.mobile = ''
|
||||
form.password = ''
|
||||
form.header = ''
|
||||
form.sex = ''
|
||||
form.isEnterprise = ''
|
||||
form.enabled = '0'
|
||||
form.remark = ''
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data: any = await httpGet('/user/user/getUserInfo', { id: props.userId })
|
||||
if (data) {
|
||||
form.id = Number(data.id ?? props.userId)
|
||||
form.nickName = data.nickName || ''
|
||||
form.mobile = data.mobile || ''
|
||||
form.password = '' // 修改时不回显密码
|
||||
form.header = data.header || ''
|
||||
form.sex = (data.sex === 0 || data.sex === 1 ? String(data.sex) : '') as UserForm['sex']
|
||||
form.isEnterprise = (data.isEnterprise === 0 || data.isEnterprise === 1 ? String(data.isEnterprise) : '') as UserForm['isEnterprise']
|
||||
form.enabled = (data.enabled === 0 || data.enabled === 1 ? String(data.enabled) : '0') as UserForm['enabled']
|
||||
form.remark = data.remark || ''
|
||||
}
|
||||
await nextTick()
|
||||
formRef.value?.clearValidate()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '加载用户详情失败')
|
||||
close()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
// 后端 save 接口吃 UserDTO (全字段), update 接口吃 UserFeignDTO (字段子集)
|
||||
// 为安全起见, 两种模式都按字段子集发, 后端可接受的最小 payload
|
||||
const payload: Record<string, any> = {
|
||||
id: form.id,
|
||||
nickName: form.nickName.trim(),
|
||||
mobile: form.mobile.trim()
|
||||
}
|
||||
if (form.password) payload.password = form.password
|
||||
if (form.header) payload.header = form.header
|
||||
if (form.sex !== '') payload.sex = Number(form.sex)
|
||||
if (form.isEnterprise !== '') payload.isEnterprise = Number(form.isEnterprise)
|
||||
if (form.enabled !== '') payload.enabled = Number(form.enabled)
|
||||
if (form.remark) payload.remark = form.remark
|
||||
|
||||
if (mode.value === 'create') {
|
||||
await httpPost('/user/user/save', payload)
|
||||
ElMessage.success('新建成功')
|
||||
} else {
|
||||
await httpPost('/user/user/update', payload)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
close()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || (mode.value === 'create' ? '新建失败' : '修改失败'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
:title="mode === 'create' ? '新建用户' : '修改用户'"
|
||||
width="560"
|
||||
:style="{ borderRadius: '12px', overflow: 'hidden' }"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@update:model-value="(v: boolean) => emit('update:visible', v)"
|
||||
@closed="resetForm"
|
||||
>
|
||||
<div v-loading="loading">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
label-position="right"
|
||||
>
|
||||
<el-form-item label="用户昵称" prop="nickName">
|
||||
<el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="50" show-word-limit clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手机号" prop="mobile">
|
||||
<el-input v-model="form.mobile" placeholder="请输入手机号" maxlength="11" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="登录密码" prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
show-password
|
||||
:placeholder="mode === 'create' ? '请输入密码 (6-20 位)' : '留空表示不修改密码'"
|
||||
maxlength="20"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="头像 URL">
|
||||
<el-input v-model="form.header" placeholder="选填, 头像图片地址" maxlength="200" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="性别">
|
||||
<el-radio-group v-model="form.sex">
|
||||
<el-radio value="">未知</el-radio>
|
||||
<el-radio value="0">男</el-radio>
|
||||
<el-radio value="1">女</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="是否企业认证">
|
||||
<el-radio-group v-model="form.isEnterprise">
|
||||
<el-radio value="">未设置</el-radio>
|
||||
<el-radio value="0">否</el-radio>
|
||||
<el-radio value="1">是</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="禁用标识">
|
||||
<el-radio-group v-model="form.enabled">
|
||||
<el-radio value="0">正常</el-radio>
|
||||
<el-radio value="1">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="选填" maxlength="200" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="close">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">
|
||||
{{ mode === 'create' ? '确认新建' : '确认修改' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@ -5,13 +5,14 @@
|
||||
* 按 ui-preview-v2.html 模板: 9 列 + 6 搜索 + 4 操作 + 状态 tab
|
||||
*/
|
||||
import { onMounted, ref, reactive, computed } from 'vue'
|
||||
import { httpGet, httpPost } from '@/utils/request'
|
||||
import { http, httpGet, httpPost } from '@/utils/request'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||
import ListTabs from '@/components/ListTabs/ListTabs.vue'
|
||||
import UserFormDialog from './UserFormDialog.vue'
|
||||
|
||||
interface FrontUser {
|
||||
id: string
|
||||
@ -33,6 +34,12 @@ const total = ref(0)
|
||||
const enabledStats = reactive({ normal: 0, disabled: 0 })
|
||||
const loading = ref(false)
|
||||
const activeTab = ref('all')
|
||||
|
||||
// 表单弹窗状态
|
||||
const dialogVisible = ref(false)
|
||||
const editingUserId = ref<string | number | null>(null)
|
||||
const exporting = ref(false)
|
||||
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
@ -192,7 +199,62 @@ function onReset() {
|
||||
}
|
||||
|
||||
function onEdit(row: FrontUser) {
|
||||
ElMessageBox.alert(`用户详情编辑待 W17 完善 (当前用户: ${row.nickName}, ID: ${row.id})`, '修改用户', { type: 'info' })
|
||||
editingUserId.value = row.id
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function onCreate() {
|
||||
editingUserId.value = null
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function onDialogSuccess() {
|
||||
load()
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出当前搜索条件下的用户列表为 Excel
|
||||
* 后端: GET /user/user/export (返 Excel 文件流, Content-Type: application/vnd.ms-excel)
|
||||
* 走 http 实例 (自动带 Bearer token), responseType: 'blob' 接二进制流
|
||||
*/
|
||||
async function exportExcel() {
|
||||
exporting.value = true
|
||||
try {
|
||||
const params = buildSearchParams()
|
||||
if (query.enabled !== '') params.enabled = Number(query.enabled)
|
||||
const resp = await http.get<Blob>('/user/user/export', {
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
const blob = resp.data
|
||||
if (!blob || blob.size === 0) {
|
||||
ElMessage.error('导出内容为空')
|
||||
return
|
||||
}
|
||||
// 优先从响应头取文件名 (后端 ExcelUtils 会设置)
|
||||
const dispo = resp.headers?.['content-disposition'] || resp.headers?.['Content-Disposition']
|
||||
let filename = `用户列表_${new Date().toISOString().slice(0, 10)}.xls`
|
||||
if (typeof dispo === 'string') {
|
||||
const m = dispo.match(/filename\*?=(?:UTF-8\u0027\u0027)?["']?([^;"']+)["']?/i)
|
||||
if (m && m[1]) {
|
||||
try { filename = decodeURIComponent(m[1]) } catch { filename = m[1] }
|
||||
}
|
||||
}
|
||||
const url = URL.createObjectURL(new Blob([blob]))
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
ElMessage.success(`已导出 ${filename}`)
|
||||
} catch (e: any) {
|
||||
const msg = e?.msg || e?.message || '导出失败'
|
||||
ElMessage.error(msg)
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleEnabled(row: FrontUser) {
|
||||
@ -230,12 +292,17 @@ onMounted(load)
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<PageHeader title="用户列表" :sub="`管理前台注册用户,共 ${enabledStats.normal + enabledStats.disabled} 个账号`">
|
||||
<el-button @click="ElMessageBox.alert('导出功能待 W17 完善', '提示', { type: 'info' })">导出</el-button>
|
||||
<el-button type="primary" @click="ElMessageBox.alert('新增用户功能待 W17 完善', '提示', { type: 'info' })">
|
||||
<el-button @click="exportExcel" :loading="exporting">导出</el-button>
|
||||
<el-button type="primary" @click="onCreate">
|
||||
<el-icon style="margin-right: 4px; vertical-align: -2px;"><Plus /></el-icon>
|
||||
新建用户
|
||||
</el-button>
|
||||
</PageHeader>
|
||||
<UserFormDialog
|
||||
v-model:visible="dialogVisible"
|
||||
:user-id="editingUserId"
|
||||
@success="onDialogSuccess"
|
||||
/>
|
||||
|
||||
<ListTabs v-model="activeTab" :items="tabItems" @change="onTabChange" />
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user