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
|
* 按 ui-preview-v2.html 模板: 9 列 + 6 搜索 + 4 操作 + 状态 tab
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, reactive, computed } from 'vue'
|
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 { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus } from '@element-plus/icons-vue'
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
import ListTabs from '@/components/ListTabs/ListTabs.vue'
|
import ListTabs from '@/components/ListTabs/ListTabs.vue'
|
||||||
|
import UserFormDialog from './UserFormDialog.vue'
|
||||||
|
|
||||||
interface FrontUser {
|
interface FrontUser {
|
||||||
id: string
|
id: string
|
||||||
@ -33,6 +34,12 @@ const total = ref(0)
|
|||||||
const enabledStats = reactive({ normal: 0, disabled: 0 })
|
const enabledStats = reactive({ normal: 0, disabled: 0 })
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const activeTab = ref('all')
|
const activeTab = ref('all')
|
||||||
|
|
||||||
|
// 表单弹窗状态
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const editingUserId = ref<string | number | null>(null)
|
||||||
|
const exporting = ref(false)
|
||||||
|
|
||||||
const query = reactive({
|
const query = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
@ -192,7 +199,62 @@ function onReset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onEdit(row: FrontUser) {
|
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) {
|
async function toggleEnabled(row: FrontUser) {
|
||||||
@ -230,12 +292,17 @@ onMounted(load)
|
|||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<PageHeader title="用户列表" :sub="`管理前台注册用户,共 ${enabledStats.normal + enabledStats.disabled} 个账号`">
|
<PageHeader title="用户列表" :sub="`管理前台注册用户,共 ${enabledStats.normal + enabledStats.disabled} 个账号`">
|
||||||
<el-button @click="ElMessageBox.alert('导出功能待 W17 完善', '提示', { type: 'info' })">导出</el-button>
|
<el-button @click="exportExcel" :loading="exporting">导出</el-button>
|
||||||
<el-button type="primary" @click="ElMessageBox.alert('新增用户功能待 W17 完善', '提示', { type: 'info' })">
|
<el-button type="primary" @click="onCreate">
|
||||||
<el-icon style="margin-right: 4px; vertical-align: -2px;"><Plus /></el-icon>
|
<el-icon style="margin-right: 4px; vertical-align: -2px;"><Plus /></el-icon>
|
||||||
新建用户
|
新建用户
|
||||||
</el-button>
|
</el-button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
<UserFormDialog
|
||||||
|
v-model:visible="dialogVisible"
|
||||||
|
:user-id="editingUserId"
|
||||||
|
@success="onDialogSuccess"
|
||||||
|
/>
|
||||||
|
|
||||||
<ListTabs v-model="activeTab" :items="tabItems" @change="onTabChange" />
|
<ListTabs v-model="activeTab" :items="tabItems" @change="onTabChange" />
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user