增加宝贝监护功能
This commit is contained in:
parent
07b95be324
commit
649da2a6fd
1
components.d.ts
vendored
1
components.d.ts
vendored
@ -55,6 +55,7 @@ declare module 'vue' {
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
|
||||
145
src/api/babyhealth.js
Normal file
145
src/api/babyhealth.js
Normal file
@ -0,0 +1,145 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/*************************************************
|
||||
****************** 宝贝相关接口 ******************
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* 获取宝贝列表
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getBabyList() {
|
||||
return request({
|
||||
url: "/admin/babys/list",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取宝贝详情
|
||||
* @param {number} id 宝贝ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getDetail(id) {
|
||||
return request({
|
||||
url: `/admin/babys/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建宝贝数据
|
||||
* @param {Object} data 宝贝数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function create(data) {
|
||||
return request({
|
||||
url: "/admin/babys",
|
||||
method: "post",
|
||||
data: data,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新宝贝数据
|
||||
* @param {number} id 宝贝ID
|
||||
* @param {Object} data 更新的数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function update(id, data) {
|
||||
return request({
|
||||
url: `/admin/babys/${id}`,
|
||||
method: "put",
|
||||
data: data,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除宝贝数据
|
||||
* @param {number} id 宝贝ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function del(id) {
|
||||
return request({
|
||||
url: `/admin/babys/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
/*************************************************
|
||||
****************** 用户相关接口 ******************
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getUserList() {
|
||||
return request({
|
||||
url: "/admin/babyhealthUser/list",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户详情
|
||||
* @param {number} id 用户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getUserDetail(id) {
|
||||
return request({
|
||||
url: `/admin/babyhealthUser/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户数据
|
||||
* @param {Object} data 用户数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function createUser(data) {
|
||||
return request({
|
||||
url: "/admin/babyhealthUser",
|
||||
method: "post",
|
||||
data: data,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户数据
|
||||
* @param {number} id 用户ID
|
||||
* @param {Object} data 更新的数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateUser(id, data) {
|
||||
return request({
|
||||
url: `/admin/babyhealthUser/${id}`,
|
||||
method: "put",
|
||||
data: data,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户数据
|
||||
* @param {number} id 用户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteUser(id) {
|
||||
return request({
|
||||
url: `/admin/babyhealthUser/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
@ -157,3 +157,39 @@ export function moveFile(id, cate) {
|
||||
params: { cate },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传头像
|
||||
* @param {FormData} formData 文件数据
|
||||
* @param {Object} options 额外选项
|
||||
* @param {string} [options.cate]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function uploadAvatar(formData, options = {}) {
|
||||
if (options.cate) {
|
||||
formData.append('cate', options.cate);
|
||||
}
|
||||
|
||||
return request({
|
||||
url: "/admin/uploadavatar",
|
||||
method: "post",
|
||||
data: formData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新头像
|
||||
* @param {number|string} id 文件ID
|
||||
* @param {Object} fileData 更新头像
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateAvatar(id, fileData) {
|
||||
return request({
|
||||
url: `/admin/uploadavatar/${id}`,
|
||||
method: "post",
|
||||
data: fileData,
|
||||
});
|
||||
}
|
||||
42
src/api/upload.js
Normal file
42
src/api/upload.js
Normal file
@ -0,0 +1,42 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
/**
|
||||
* 上传头像
|
||||
* @param {FormData} data 表单数据
|
||||
* @param {Object} params 查询参数
|
||||
* @returns {Promise} Promise对象
|
||||
*/
|
||||
export function uploadAvatar(data, params) {
|
||||
return request({
|
||||
url: '/admin/uploadavatar',
|
||||
method: 'post',
|
||||
data,
|
||||
params,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param {FormData} data 表单数据
|
||||
* @param {Object} params 查询参数
|
||||
* @returns {Promise} Promise对象
|
||||
*/
|
||||
export function uploadFile(data, params) {
|
||||
return request({
|
||||
url: '/admin/upload',
|
||||
method: 'post',
|
||||
data,
|
||||
params,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
uploadAvatar,
|
||||
uploadFile
|
||||
};
|
||||
28
src/env.d.ts
vendored
Normal file
28
src/env.d.ts
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
declare module '@/*' {
|
||||
import type { ComponentOptions } from 'vue';
|
||||
const component: ComponentOptions;
|
||||
export default component;
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL: string;
|
||||
// 添加其他环境变量...
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
declare module 'vue-cropper' {
|
||||
import { DefineComponent } from 'vue';
|
||||
const VueCropper: DefineComponent<{}, {}, any>;
|
||||
export default VueCropper;
|
||||
}
|
||||
376
src/views/apps/babyhealth/babys/components/edit.vue
Normal file
376
src/views/apps/babyhealth/babys/components/edit.vue
Normal file
@ -0,0 +1,376 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑宝贝' : '添加宝贝'"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="editFormRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入姓名" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="昵称" prop="nickname">
|
||||
<el-input v-model="formData.nickname" placeholder="请输入昵称" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="性别" prop="sex">
|
||||
<el-radio-group v-model="formData.sex">
|
||||
<el-radio :value="1">男</el-radio>
|
||||
<el-radio :value="2">女</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="出生日期" prop="birth">
|
||||
<el-date-picker
|
||||
v-model="formData.birth"
|
||||
type="date"
|
||||
placeholder="选择出生日期"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="身高" prop="height">
|
||||
<el-input-number
|
||||
v-model="formData.height"
|
||||
:min="0"
|
||||
:max="200"
|
||||
:precision="1"
|
||||
:step="0.5"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<span style="margin-left: 8px; color: var(--el-text-color-regular)">cm</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="体重" prop="weight">
|
||||
<el-input-number
|
||||
v-model="formData.weight"
|
||||
:min="0"
|
||||
:max="50"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<span style="margin-left: 8px; color: var(--el-text-color-regular)">kg</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="头像">
|
||||
<el-upload
|
||||
:action="uploadUrl"
|
||||
:show-file-list="false"
|
||||
:on-success="handleAvatarSuccess"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
:http-request="uploadFile"
|
||||
:headers="uploadHeaders"
|
||||
>
|
||||
<img v-if="formData.avatar" :src="getEnvUrl(formData.avatar)" class="avatar-preview" />
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="1">正常</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
|
||||
<!-- 预览对话框 -->
|
||||
<el-dialog v-model="previewVisible" title="头像预览" width="500px">
|
||||
<img w-full :src="getEnvUrl(previewImageUrl)" alt="Preview Image" style="width: 100%" />
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { createBaby, updateBaby } from '@/api/babyhealth';
|
||||
import { uploadAvatar } from '@/api/upload';
|
||||
|
||||
interface BabyFormData {
|
||||
id: number;
|
||||
name: string;
|
||||
nickname: string;
|
||||
sex: number;
|
||||
birth: string;
|
||||
height: number;
|
||||
weight: number;
|
||||
avatar: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
editData?: BabyFormData;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
submit: [data: BabyFormData];
|
||||
}>();
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const previewVisible = ref(false);
|
||||
const previewImageUrl = ref('');
|
||||
const loading = ref(false);
|
||||
const editFormRef = ref<FormInstance>();
|
||||
const fileList = ref<any[]>([]);
|
||||
|
||||
const formData = reactive<BabyFormData>({
|
||||
id: 0,
|
||||
name: '',
|
||||
nickname: '',
|
||||
sex: 1,
|
||||
birth: '',
|
||||
height: 0,
|
||||
weight: 0,
|
||||
avatar: '',
|
||||
status: 1
|
||||
});
|
||||
|
||||
const isEdit = ref(false);
|
||||
|
||||
const formRules: FormRules = {
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '请选择性别', trigger: 'change' }],
|
||||
birth: [{ required: true, message: '请选择出生日期', trigger: 'change' }]
|
||||
};
|
||||
|
||||
// 拼接接口路径
|
||||
const getEnvUrl = (path: string) => {
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
return `${API_BASE_URL}${path}`;
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
id: 0,
|
||||
name: '',
|
||||
nickname: '',
|
||||
sex: 1,
|
||||
birth: '',
|
||||
height: 0,
|
||||
weight: 0,
|
||||
avatar: '',
|
||||
status: 1
|
||||
});
|
||||
fileList.value = [];
|
||||
};
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (val) => {
|
||||
dialogVisible.value = val;
|
||||
});
|
||||
|
||||
// 监听 dialogVisible 变化
|
||||
watch(dialogVisible, (val) => {
|
||||
emit('update:visible', val);
|
||||
});
|
||||
|
||||
// 监听 editData 变化
|
||||
watch(() => props.editData, (data) => {
|
||||
if (data) {
|
||||
isEdit.value = true;
|
||||
Object.assign(formData, data);
|
||||
if (data.avatar) {
|
||||
fileList.value = [
|
||||
{
|
||||
name: 'avatar',
|
||||
url: data.avatar
|
||||
}
|
||||
];
|
||||
} else {
|
||||
fileList.value = [];
|
||||
}
|
||||
} else {
|
||||
isEdit.value = false;
|
||||
resetForm();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = import.meta.env.VITE_API_BASE_URL + "/admin/uploadavatar";
|
||||
const uploadHeaders = {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
};
|
||||
|
||||
// 头像上传前校验
|
||||
const beforeAvatarUpload = (file: any) => {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('仅支持图片格式');
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// 上传文件到服务器
|
||||
const uploadFile = async (options: any) => {
|
||||
const { file } = options;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
// 使用uploadAvatar接口上传文件
|
||||
const response = await uploadAvatar(formData, { cate: 'baby_avatar' });
|
||||
|
||||
if (response.code === 200) {
|
||||
// 更新宝贝头像
|
||||
const avatarUrl = response.data.url || response.data.path || '';
|
||||
formData.avatar = avatarUrl;
|
||||
|
||||
// 如果正在编辑,则更新宝贝信息
|
||||
if (formData.id) {
|
||||
await updateBaby(formData.id, { avatar: avatarUrl });
|
||||
}
|
||||
|
||||
ElMessage.success("头像上传成功");
|
||||
} else {
|
||||
ElMessage.error(response.msg || "上传失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
ElMessage.error("上传失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 上传成功
|
||||
const handleAvatarSuccess = (response: any) => {
|
||||
if (response.code === 200) {
|
||||
const avatarUrl = response.data.url || response.data.path || '';
|
||||
formData.avatar = avatarUrl;
|
||||
|
||||
// 如果正在编辑,则更新宝贝信息
|
||||
if (formData.id) {
|
||||
updateBaby(formData.id, { avatar: avatarUrl });
|
||||
}
|
||||
|
||||
ElMessage.success("头像上传成功");
|
||||
} else {
|
||||
ElMessage.error(response.msg || "上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 上传相关
|
||||
const handleUploadChange = (file: any) => {
|
||||
if (file.raw) {
|
||||
const isImage = file.raw.type.startsWith('image/');
|
||||
const isLt5M = file.raw.size / 1024 / 1024 < 5;
|
||||
if (!isImage || !isLt5M) {
|
||||
fileList.value = [];
|
||||
ElMessage.error(isImage ? '图片大小不能超过5MB' : '仅支持图片格式');
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (file: any) => {
|
||||
fileList.value = [];
|
||||
formData.avatar = '';
|
||||
};
|
||||
|
||||
const handlePictureCardPreview = (file: any) => {
|
||||
previewImageUrl.value = file.url;
|
||||
previewVisible.value = true;
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!editFormRef.value) return;
|
||||
|
||||
await editFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 如果有新上传的图片,先上传
|
||||
if (fileList.value.length > 0 && fileList.value[0].raw) {
|
||||
const uploadFormData = new FormData();
|
||||
uploadFormData.append('file', fileList.value[0].raw);
|
||||
uploadFormData.append('cate', 'baby');
|
||||
|
||||
const uploadRes = await uploadAvatar(uploadFormData);
|
||||
if (uploadRes.code === 200 && uploadRes.data?.url) {
|
||||
formData.avatar = uploadRes.data.url;
|
||||
} else {
|
||||
ElMessage.error('图片上传失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 提交数据
|
||||
emit('submit', { ...formData });
|
||||
dialogVisible.value = false;
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
ElMessage.error('提交失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.avatar-preview {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.avatar-uploader {
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
text-align: center;
|
||||
line-height: 100px;
|
||||
}
|
||||
}
|
||||
:deep(.el-upload-list__item-thumbnail) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
400
src/views/apps/babyhealth/babys/index.vue
Normal file
400
src/views/apps/babyhealth/babys/index.vue
Normal file
@ -0,0 +1,400 @@
|
||||
<template>
|
||||
<div class="babys-container">
|
||||
<div class="header-section">
|
||||
<h2 class="page-title">宝贝管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加宝贝
|
||||
</el-button>
|
||||
<el-button @click="handleRefresh">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<div v-loading="loading" class="babys-grid">
|
||||
<div v-for="baby in babyList" :key="baby.id" class="baby-card">
|
||||
<div class="baby-avatar">
|
||||
<img v-if="baby.avatar" :src="getEnvUrl(baby.avatar)" :alt="baby.name" />
|
||||
<div v-else class="avatar-placeholder">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="baby-content">
|
||||
<div class="names">
|
||||
<h3 class="baby-name" :class="'sex-'+baby.sex">
|
||||
<i class="fa-solid fa-mars"></i> {{ baby.name }}
|
||||
</h3>
|
||||
<div class="birth">
|
||||
<el-tag><i class="fa-solid fa-cake-candles"></i> {{ formatDate(baby.birth) }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="baby-info">
|
||||
<div class="info-left">
|
||||
<div class="info-item">
|
||||
<span><strong>父亲:</strong> {{ baby.father || '未绑定' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span><strong>母亲:</strong> {{ baby.mather || '未绑定' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-right">
|
||||
<div class="info-item">
|
||||
<span><strong>身高:</strong> {{ baby.height || 0 }}cm</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span><strong>体重:</strong> {{ baby.weight || 0 }}kg</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="baby-footer">
|
||||
<el-tag :type="baby.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ baby.status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
<div class="baby-actions">
|
||||
<el-button size="small" type="primary" link @click="handleView(baby)">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleEdit(baby)">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(baby)">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="babyList.length === 0 && !loading" class="empty-state">
|
||||
<el-empty description="暂无宝贝数据" :image-size="120" />
|
||||
</div>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<BabyEdit
|
||||
v-model:visible="editDialogVisible"
|
||||
:editData="currentBaby"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import BabyEdit from './components/edit.vue';
|
||||
import { getBabyList, getDetail, create, update, del } from '@/api/babyhealth';
|
||||
|
||||
interface Baby {
|
||||
id: number;
|
||||
name: string;
|
||||
nickname: string;
|
||||
sex: number;
|
||||
birth: string;
|
||||
height: number;
|
||||
weight: number;
|
||||
avatar: string;
|
||||
status: number;
|
||||
create_time: string;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const babyList = ref<Baby[]>([]);
|
||||
const editDialogVisible = ref(false);
|
||||
const currentBaby = ref<Baby | null>(null);
|
||||
|
||||
// 拼接接口路径
|
||||
const getEnvUrl = (path: string) => {
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
return `${API_BASE_URL}${path}`;
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: string) => {
|
||||
if (!date) return '-';
|
||||
return date.substring(0, 10);
|
||||
};
|
||||
|
||||
// 格式化性别
|
||||
const formatGender = (gender: number) => {
|
||||
const map: Record<number, string> = {
|
||||
1: '男',
|
||||
2: '女'
|
||||
};
|
||||
return map[gender] || '未知';
|
||||
};
|
||||
|
||||
// 刷新列表
|
||||
const handleRefresh = () => {
|
||||
fetchBabyList();
|
||||
};
|
||||
|
||||
// 添加宝贝
|
||||
const handleAdd = () => {
|
||||
currentBaby.value = null;
|
||||
editDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const handleView = (row: Baby) => {
|
||||
ElMessage.info('详情功能待实现');
|
||||
};
|
||||
|
||||
// 编辑宝贝
|
||||
const handleEdit = (row: Baby) => {
|
||||
currentBaby.value = row;
|
||||
editDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除宝贝
|
||||
const handleDelete = (row: Baby) => {
|
||||
ElMessageBox.confirm(`确定要删除宝贝「${row.name}」吗?删除后不可恢复。`, '警告', {
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await del(row.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功');
|
||||
fetchBabyList();
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
ElMessage.error('删除失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async (data: any) => {
|
||||
try {
|
||||
if (data.id) {
|
||||
// 编辑
|
||||
const res = await update(data.id, data);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('编辑成功');
|
||||
fetchBabyList();
|
||||
} else {
|
||||
ElMessage.error(res.msg || '编辑失败');
|
||||
}
|
||||
} else {
|
||||
// 添加
|
||||
const res = await create(data);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('添加成功');
|
||||
fetchBabyList();
|
||||
} else {
|
||||
ElMessage.error(res.msg || '添加失败');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(data.id ? '编辑失败:' : '添加失败:', error);
|
||||
ElMessage.error(data.id ? '编辑失败' : '添加失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 获取宝贝列表
|
||||
const fetchBabyList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getBabyList();
|
||||
console.log('获取宝贝列表响应:', res);
|
||||
if (res.code === 200) {
|
||||
babyList.value = res.data || [];
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取宝贝列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取宝贝列表失败:', error);
|
||||
ElMessage.error('获取宝贝列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchBabyList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.babys-container {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
min-height: 100vh;
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.babys-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
|
||||
.baby-card {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.3s;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-4px);
|
||||
border-color: var(--el-color-primary-light-7);
|
||||
}
|
||||
|
||||
.baby-avatar {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.fa-solid {
|
||||
font-size: 40px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.baby-content {
|
||||
padding: 20px;
|
||||
|
||||
.names{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.baby-name {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
// color: var(--el-text-color-primary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.baby-nickname {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.baby-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.info-left,
|
||||
.info-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
.fa-solid {
|
||||
font-size: 16px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.baby-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.baby-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 80px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sex-0,.sex-1,.sex-2{
|
||||
|
||||
}
|
||||
|
||||
.sex-0{
|
||||
|
||||
}
|
||||
.sex-1{
|
||||
|
||||
color: #409eff;
|
||||
}
|
||||
.sex-2{
|
||||
color: #ff69b4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
196
src/views/apps/babyhealth/dashborad/index.vue
Normal file
196
src/views/apps/babyhealth/dashborad/index.vue
Normal file
@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div class="statistics-container">
|
||||
<el-row :gutter="20" class="data-overview">
|
||||
<el-col :span="6" v-for="item in summaryData" :key="item.title">
|
||||
<el-card shadow="hover" class="data-card">
|
||||
<div class="card-content">
|
||||
<div class="icon-box" :style="{ backgroundColor: item.color }">
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="text-box">
|
||||
<div class="title">{{ item.title }}</div>
|
||||
<div class="value">{{ item.value.toLocaleString() }}</div>
|
||||
<div class="trend" :class="item.isUp ? 'up' : 'down'">
|
||||
{{ item.isUp ? '↑' : '↓' }} {{ item.percentage }}%
|
||||
<span>较上月</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="charts-row">
|
||||
<el-col :span="16">
|
||||
<el-card shadow="hover" header="用户增长趋势">
|
||||
<div ref="lineChartRef" class="chart-box"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" header="用户等级分布">
|
||||
<div ref="pieChartRef" class="chart-box"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, shallowRef } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { User, Pointer, Connection, Histogram } from '@element-plus/icons-vue';
|
||||
|
||||
// --- 类型定义 ---
|
||||
interface SummaryItem {
|
||||
title: string;
|
||||
value: number;
|
||||
icon: any;
|
||||
color: string;
|
||||
percentage: number;
|
||||
isUp: boolean;
|
||||
}
|
||||
|
||||
// --- 响应式数据 ---
|
||||
const lineChartRef = ref<HTMLElement | null>(null);
|
||||
const pieChartRef = ref<HTMLElement | null>(null);
|
||||
const lineChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
const pieChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
|
||||
const summaryData = ref<SummaryItem[]>([
|
||||
{ title: '总用户数', value: 12840, icon: User, color: '#3973FF', percentage: 12, isUp: true },
|
||||
{ title: '今日新增', value: 156, icon: Pointer, color: '#67C23A', percentage: 5, isUp: true },
|
||||
{ title: '活跃用户', value: 3420, icon: Connection, color: '#E6A23C', percentage: 2, isUp: false },
|
||||
{ title: '留存率', value: 85, icon: Histogram, color: '#F56C6C', percentage: 1, isUp: true },
|
||||
]);
|
||||
|
||||
// --- 初始化图表 ---
|
||||
const initCharts = () => {
|
||||
// 折线图配置
|
||||
if (lineChartRef.value) {
|
||||
lineChartInstance.value = echarts.init(lineChartRef.value);
|
||||
lineChartInstance.value.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
},
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{
|
||||
name: '新增用户',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [120, 132, 101, 134, 90, 230, 210],
|
||||
areaStyle: { opacity: 0.3 },
|
||||
itemStyle: { color: '#3973FF' }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// 饼图配置
|
||||
if (pieChartRef.value) {
|
||||
pieChartInstance.value = echarts.init(pieChartRef.value);
|
||||
pieChartInstance.value.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: '0%', left: 'center' },
|
||||
series: [
|
||||
{
|
||||
name: '等级分布',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { show: false },
|
||||
data: [
|
||||
{ value: 1048, name: '普通用户' },
|
||||
{ value: 735, name: 'VIP会员' },
|
||||
{ value: 580, name: '超级管理员' },
|
||||
{ value: 484, name: '运营人员' }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// --- 生命周期与自适应 ---
|
||||
const handleResize = () => {
|
||||
lineChartInstance.value?.resize();
|
||||
pieChartInstance.value?.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initCharts();
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.statistics-container {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
|
||||
.data-overview {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.data-card {
|
||||
.card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon-box {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 15px;
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.text-box {
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
.value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 4px 0;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.trend {
|
||||
font-size: 12px;
|
||||
&.up { color: #67c23a; }
|
||||
&.down { color: #f56c6c; }
|
||||
span { color: #909399; margin-left: 4px; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
.chart-box {
|
||||
height: 350px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 深度修改 Element Plus 卡片头部样式
|
||||
:deep(.el-card__header) {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
</style>
|
||||
8
src/views/apps/babyhealth/index.vue
Normal file
8
src/views/apps/babyhealth/index.vue
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
198
src/views/apps/babyhealth/users/components/changePassword.vue
Normal file
198
src/views/apps/babyhealth/users/components/changePassword.vue
Normal file
@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="修改密码" width="400px" @close="handleClose">
|
||||
<el-form :model="form">
|
||||
<!-- 用户账号(只读) -->
|
||||
<el-form-item label="账号">
|
||||
<el-input v-model="form.username" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 新密码 -->
|
||||
<el-form-item label="新密码">
|
||||
<el-input
|
||||
v-model="passwordForm.newPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
placeholder="请输入新密码(6-16位)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<el-form-item label="确认密码">
|
||||
<el-input
|
||||
v-model="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<el-form-item v-if="passwordError">
|
||||
<el-alert :title="passwordError" type="error" :closable="false" style="color: #f56c6c;" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 对话框脚部 -->
|
||||
<template #footer>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定修改</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { changePassword } from "@/api/user";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
userId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'submit', 'close']);
|
||||
|
||||
const visible = ref(false);
|
||||
const passwordError = ref("");
|
||||
|
||||
const form = ref<any>({
|
||||
id: null,
|
||||
username: "",
|
||||
});
|
||||
|
||||
const passwordForm = ref<any>({
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
// 监听 modelValue
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal;
|
||||
if (newVal && props.userId) {
|
||||
form.value.id = props.userId;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 userId 变化
|
||||
watch(() => props.userId, (newVal) => {
|
||||
if (newVal) {
|
||||
form.value.id = newVal;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
if (!newVal) {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
});
|
||||
|
||||
// 校验密码
|
||||
const validatePassword = (password: string) => {
|
||||
if (!password) {
|
||||
return "请输入密码";
|
||||
}
|
||||
if (password.length < 6) {
|
||||
return "密码长度不能小于6位";
|
||||
}
|
||||
if (password.length > 16) {
|
||||
return "密码长度不能大于16位";
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 校验确认密码
|
||||
const validateConfirmPassword = (password: string, confirmPassword: string) => {
|
||||
if (!confirmPassword) {
|
||||
return "请再次输入密码";
|
||||
}
|
||||
if (confirmPassword !== password) {
|
||||
return "两次输入的密码不一致";
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
passwordForm.value = {
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
passwordError.value = "";
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 清除之前的错误
|
||||
passwordError.value = "";
|
||||
|
||||
try {
|
||||
if (!form.value.id) {
|
||||
passwordError.value = "用户ID不能为空";
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验新密码格式
|
||||
const passwordCheck = validatePassword(passwordForm.value.newPassword);
|
||||
if (passwordCheck !== true) {
|
||||
passwordError.value = passwordCheck;
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验确认密码
|
||||
const confirmCheck = validateConfirmPassword(
|
||||
passwordForm.value.newPassword,
|
||||
passwordForm.value.confirmPassword
|
||||
);
|
||||
if (confirmCheck !== true) {
|
||||
passwordError.value = confirmCheck;
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用接口修改密码
|
||||
const res = await changePassword(form.value.id, passwordForm.value);
|
||||
|
||||
if (res.code === 200 || res.msg === '修改成功') {
|
||||
ElMessage.success("密码修改成功");
|
||||
visible.value = false;
|
||||
emit('update:modelValue', false);
|
||||
emit('submit');
|
||||
} else {
|
||||
passwordError.value = res.msg || "密码修改失败";
|
||||
}
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.response?.data?.msg || e?.response?.data?.message || e?.message || "操作失败";
|
||||
passwordError.value = errorMsg;
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
open: (userId: number, username: string) => {
|
||||
form.value = {
|
||||
id: userId,
|
||||
username: username,
|
||||
};
|
||||
passwordForm.value = {
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
passwordError.value = "";
|
||||
visible.value = true;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
190
src/views/apps/babyhealth/users/components/preview.vue
Normal file
190
src/views/apps/babyhealth/users/components/preview.vue
Normal file
@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" title="用户信息预览" size="50%">
|
||||
<div class="user-preview" v-if="user">
|
||||
<div class="user-header">
|
||||
<div class="user-avatar">
|
||||
<el-avatar :size="80" :icon="UserFilled" />
|
||||
</div>
|
||||
<h2 class="user-name">{{ user.name || "未知用户" }}</h2>
|
||||
<el-tag :type="user.status === 1 ? 'success' : 'danger'" size="large">
|
||||
{{ user.status === 1 ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<div class="user-info">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="ID">
|
||||
{{ user.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="账号">
|
||||
{{ user.account || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">
|
||||
{{ user.name || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">
|
||||
{{ user.sex === 1 ? "男" : "女" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">
|
||||
{{ user.phone || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="QQ">
|
||||
{{ user.qq || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">
|
||||
{{ user.email || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="角色">
|
||||
<el-tag :type="getRoleTagType(user.group_id)" size="small">
|
||||
{{ getRoleName(user.group_id) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最后登录IP">
|
||||
{{ user.last_login_ip || "未登录" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="登录次数">
|
||||
{{ user.login_count || 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ user.create_time || "未知" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">
|
||||
{{ user.update_time || "未知" }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-user">
|
||||
<el-empty description="暂无用户信息" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { UserFilled } from "@element-plus/icons-vue";
|
||||
import { getAllRoles } from "@/api/role";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
account: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
qq: string;
|
||||
email: string;
|
||||
sex: number;
|
||||
group_id: number;
|
||||
status: number;
|
||||
last_login_ip: string;
|
||||
login_count: number;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
user: {
|
||||
type: Object as () => User | undefined,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const roles = ref<Role[]>([]);
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
visible.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
// 监听visible变化,同步给父组件
|
||||
watch(visible, (newVal) => {
|
||||
emit("update:modelValue", newVal);
|
||||
});
|
||||
|
||||
// 获取角色列表
|
||||
const fetchRoles = async () => {
|
||||
try {
|
||||
const res = await getAllRoles();
|
||||
roles.value = res.data || [];
|
||||
} catch (e) {
|
||||
roles.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 获取角色tag状态
|
||||
function getRoleTagType(group_id: number): string {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: "primary",
|
||||
2: "success",
|
||||
3: "warning",
|
||||
4: "danger",
|
||||
};
|
||||
return typeMap[group_id] || "primary";
|
||||
}
|
||||
|
||||
// 获取角色名称
|
||||
function getRoleName(group_id: number): string {
|
||||
const role = roles.value.find((r) => r.id === group_id);
|
||||
return role?.name || "未知";
|
||||
}
|
||||
|
||||
// 暴露open方法供父组件调用
|
||||
const open = (userData?: User) => {
|
||||
visible.value = true;
|
||||
fetchRoles();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
});
|
||||
|
||||
// 初始化时获取角色列表
|
||||
fetchRoles();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-preview {
|
||||
padding: 20px;
|
||||
|
||||
.user-header {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
|
||||
.user-avatar {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
margin: 15px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-user {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
539
src/views/apps/babyhealth/users/components/userEdit.vue
Normal file
539
src/views/apps/babyhealth/users/components/userEdit.vue
Normal file
@ -0,0 +1,539 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" :title="dialogTitle" width="500px">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<div class="form-title">账号信息</div>
|
||||
<!-- 账号 -->
|
||||
<el-form-item label="账号">
|
||||
<el-input v-model="form.account" :disabled="!isAdd" placeholder="请输入账号" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 密码 -->
|
||||
<el-form-item label="密码" prop="password" v-if="isAdd">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
:placeholder="isAdd ? '请输入密码(至少6位)' : '留空则不修改密码'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<el-form-item label="确认密码" prop="confirmPassword" v-if="isAdd">
|
||||
<el-input
|
||||
v-model="form.confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
:placeholder="isAdd ? '请再次输入密码' : '留空则不修改密码'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider></el-divider>
|
||||
<div class="form-title">个人信息</div>
|
||||
<!-- 头像 -->
|
||||
<el-form-item label="头像">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:show-file-list="false"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
:http-request="uploadFile"
|
||||
>
|
||||
<img v-if="form.avatar" :src="getAvatarUrl(form.avatar)" class="avatar" />
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 姓名 -->
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="form.name" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 性别 -->
|
||||
<el-form-item label="性别">
|
||||
<el-radio-group v-model="form.sex" placeholder="请选择性别">
|
||||
<el-radio-button label="未知" :value="0" />
|
||||
<el-radio-button label="男" :value="1" />
|
||||
<el-radio-button label="女" :value="2" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 生日 -->
|
||||
<el-form-item label="生日">
|
||||
<el-date-picker
|
||||
v-model="form.birth"
|
||||
type="date"
|
||||
placeholder="请选择日期"
|
||||
style="width: 100%"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 电话 -->
|
||||
<el-form-item label="电话">
|
||||
<el-input v-model="form.phone" placeholder="请输入电话" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 邮箱 -->
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 状态 -->
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="form.status"
|
||||
placeholder="请选择状态"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 对话框脚部 -->
|
||||
<template #footer>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import type { UploadProps, UploadRequestOptions } from 'element-plus';
|
||||
import { createUser, updateUser, getUserDetail } from "@/api/babyhealth";
|
||||
import { uploadAvatar } from '@/api/upload';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
statusDict: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "submit", "close"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const formRef = ref<any>(null);
|
||||
const isAdd = ref(false);
|
||||
|
||||
const form = ref<any>({
|
||||
id: null,
|
||||
account: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
name: "",
|
||||
sex: 0,
|
||||
birth: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
status: 1,
|
||||
avatar: "",
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
return isAdd.value ? "添加用户" : "编辑用户";
|
||||
});
|
||||
|
||||
// 密码验证规则
|
||||
const validatePassword = (rule: any, value: any, callback: any) => {
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,密码必填
|
||||
if (!value) {
|
||||
callback(new Error("请输入密码"));
|
||||
return;
|
||||
}
|
||||
if (value.length < 6) {
|
||||
callback(new Error("密码长度不能少于6位"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则必须符合规则
|
||||
if (value && value.length < 6) {
|
||||
callback(new Error("密码长度不能少于6位"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
// 确认密码验证规则
|
||||
const validateConfirmPassword = (rule: any, value: any, callback: any) => {
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,确认密码必填
|
||||
if (!value) {
|
||||
callback(new Error("请再次输入密码"));
|
||||
return;
|
||||
}
|
||||
if (value !== form.value.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则确认密码必须一致
|
||||
if (form.value.password && value !== form.value.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
return;
|
||||
}
|
||||
// 如果填写了确认密码但没填密码,提示错误
|
||||
if (value && !form.value.password) {
|
||||
callback(new Error("请先输入密码"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
account: [
|
||||
{ required: true, message: "请输入账号", trigger: "blur" },
|
||||
{ min: 3, max: 20, message: "账号长度在 3 到 20 个字符", trigger: "blur" },
|
||||
],
|
||||
name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
password: [{ validator: validatePassword, trigger: "blur" }],
|
||||
confirmPassword: [{ validator: validateConfirmPassword, trigger: "blur" }],
|
||||
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }],
|
||||
};
|
||||
|
||||
// 监听 modelValue
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
visible.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
if (!newVal) {
|
||||
emit("update:modelValue", false);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 statusDict 变化,用于调试
|
||||
watch(
|
||||
() => props.statusDict,
|
||||
(newVal) => {},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = import.meta.env.VITE_API_BASE_URL + "/admin/upload";
|
||||
const uploadHeaders = {
|
||||
Authorization: "Bearer " + localStorage.getItem("token"),
|
||||
};
|
||||
|
||||
// 拼接头像完整URL
|
||||
const getAvatarUrl = (url: string) => {
|
||||
if (!url) return '';
|
||||
// 如果已经是完整URL,直接返回
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url;
|
||||
}
|
||||
// 拼接API地址
|
||||
return `${import.meta.env.VITE_API_BASE_URL}${url}`;
|
||||
};
|
||||
|
||||
// 头像上传前校验
|
||||
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||
const isImage = rawFile.type.startsWith("image/");
|
||||
const isLt5M = rawFile.size / 1024 / 1024 < 5;
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error("只能上传图片文件!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isLt5M) {
|
||||
ElMessage.error("图片大小不能超过5MB!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 上传文件到服务器
|
||||
const uploadFile = async (options: any) => {
|
||||
const { file } = options;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
// 使用uploadAvatar接口上传文件
|
||||
const response = await uploadAvatar(formData, { cate: 'user_avatar' });
|
||||
|
||||
if (response.code === 200) {
|
||||
// 更新用户头像
|
||||
form.value.avatar = response.data.url || response.data.path || '';
|
||||
// 调用更新用户信息接口保存头像URL
|
||||
await updateUser(form.value.id, { avatar: form.value.avatar });
|
||||
ElMessage.success("头像上传成功");
|
||||
} else {
|
||||
ElMessage.error(response.msg || "上传失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
ElMessage.error("上传失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 头像上传成功
|
||||
const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
|
||||
if (response.code === 200) {
|
||||
form.value.avatar = response.data.avatar || '';
|
||||
ElMessage.success("头像上传成功");
|
||||
} else {
|
||||
ElMessage.error(response.msg || "上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
const loadUserData = async (user: any) => {
|
||||
try {
|
||||
// 处理两种调用方式:传递用户对象或用户 ID
|
||||
const userId = typeof user === "number" ? user : user?.id || user?.userId;
|
||||
if (!userId) {
|
||||
throw new Error("未提供有效的用户 ID");
|
||||
}
|
||||
|
||||
const res = await getUserDetail(userId);
|
||||
|
||||
const data = res.data || res;
|
||||
|
||||
// 确保 sex 和 status 都是数字类型
|
||||
const sexValue =
|
||||
data.sex !== undefined && data.sex !== null
|
||||
? Number(data.sex)
|
||||
: 1;
|
||||
|
||||
const statusValue =
|
||||
data.status !== undefined && data.status !== null
|
||||
? Number(data.status)
|
||||
: 1;
|
||||
|
||||
form.value = {
|
||||
id: data.id,
|
||||
account: data.account,
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
name: data.name,
|
||||
sex: sexValue,
|
||||
birth: data.birth,
|
||||
phone: data.phone,
|
||||
email: data.email,
|
||||
status: statusValue,
|
||||
avatar: data.avatar || "",
|
||||
};
|
||||
} catch (e: any) {
|
||||
console.error("Failed to load user data:", e);
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
|
||||
ElMessage.error(errorMsg);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 表单验证
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch (error) {
|
||||
ElMessage.warning("请检查表单填写是否正确");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证密码一致性
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,密码必填
|
||||
if (!form.value.password) {
|
||||
ElMessage.error("请输入密码");
|
||||
return;
|
||||
}
|
||||
if (form.value.password !== form.value.confirmPassword) {
|
||||
ElMessage.error("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则必须填写确认密码且一致
|
||||
if (form.value.password) {
|
||||
if (form.value.password !== form.value.confirmPassword) {
|
||||
ElMessage.error("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (isAdd.value) {
|
||||
// 新增用户
|
||||
const submitData: any = {
|
||||
account: form.value.account,
|
||||
password: form.value.password || '',
|
||||
name: form.value.name,
|
||||
sex: form.value.sex,
|
||||
birth: form.value.birth,
|
||||
phone: form.value.phone,
|
||||
email: form.value.email,
|
||||
status: form.value.status,
|
||||
avatar: form.value.avatar,
|
||||
};
|
||||
|
||||
await createUser(submitData);
|
||||
ElMessage.success("添加成功");
|
||||
} else {
|
||||
// 编辑用户
|
||||
if (!form.value.id || form.value.id === 0) {
|
||||
ElMessage.error("用户ID不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
const submitData: any = {
|
||||
id: form.value.id,
|
||||
account: form.value.account,
|
||||
name: form.value.name,
|
||||
sex: form.value.sex,
|
||||
birth: form.value.birth,
|
||||
phone: form.value.phone,
|
||||
email: form.value.email,
|
||||
status: form.value.status,
|
||||
avatar: form.value.avatar,
|
||||
};
|
||||
|
||||
// 只有在填写了密码时才添加到提交数据中
|
||||
if (form.value.password) {
|
||||
submitData.password = form.value.password;
|
||||
}
|
||||
|
||||
await updateUser(form.value.id, submitData);
|
||||
ElMessage.success("更新成功");
|
||||
}
|
||||
|
||||
visible.value = false;
|
||||
emit("submit");
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "操作失败";
|
||||
ElMessage.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
loadUserData,
|
||||
openAdd: () => {
|
||||
isAdd.value = true;
|
||||
form.value = {
|
||||
id: 0,
|
||||
account: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
name: "",
|
||||
sex: 0,
|
||||
birth: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
status: 1,
|
||||
avatar: "",
|
||||
};
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
},
|
||||
openEdit: (user: any) => {
|
||||
isAdd.value = false;
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
// 异步加载用户详细信息
|
||||
loadUserData(user);
|
||||
},
|
||||
open: (user?: any) => {
|
||||
if (user) {
|
||||
isAdd.value = false;
|
||||
loadUserData(user);
|
||||
} else {
|
||||
isAdd.value = true;
|
||||
form.value = {
|
||||
id: 0,
|
||||
account: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
name: "",
|
||||
sex: 0,
|
||||
birth: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
status: 1,
|
||||
avatar: "",
|
||||
};
|
||||
}
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.avatar-uploader {
|
||||
.avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
text-align: center;
|
||||
line-height: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
328
src/views/apps/babyhealth/users/index.vue
Normal file
328
src/views/apps/babyhealth/users/index.vue
Normal file
@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>用户管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAddUser">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加用户
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<el-table :data="users" style="width: 100%" v-loading="loading">
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
align="center"
|
||||
fixed="left"
|
||||
/>
|
||||
<el-table-column prop="account" label="账号" align="center" />
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="姓名"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span class="name-link" @click="handlePreview(scope.row)">{{ scope.row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sex" label="性别" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="getSexTagType(scope.row.sex)"
|
||||
size="small"
|
||||
>
|
||||
{{ getSexTagText(scope.row.sex) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="phone"
|
||||
label="手机号"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="email"
|
||||
label="email"
|
||||
width="180"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{
|
||||
scope.row.status === 1 ? "启用" : "禁用"
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEdit(scope.row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
type="warning"
|
||||
@click="handleChangePassword(scope.row)"
|
||||
>
|
||||
修改密码
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
layout="total, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 编辑用户对话框组件 -->
|
||||
<UserEditDialog
|
||||
ref="userEditRef"
|
||||
:modelValue="editDialogVisible"
|
||||
@update:modelValue="editDialogVisible = $event"
|
||||
:is-edit="isEdit"
|
||||
@submit="handleEditSuccess"
|
||||
@close="editDialogVisible = false"
|
||||
/>
|
||||
|
||||
<!-- 修改密码对话框组件 -->
|
||||
<ChangePasswordDialog
|
||||
ref="changePasswordRef"
|
||||
:modelValue="passwordDialogVisible"
|
||||
@update:modelValue="passwordDialogVisible = $event"
|
||||
:user-id="currentUserId"
|
||||
@submit="handlePasswordChangeSuccess"
|
||||
@close="passwordDialogVisible = false"
|
||||
/>
|
||||
|
||||
<!-- 预览用户对话框组件 -->
|
||||
<PreviewDialog
|
||||
ref="previewDialogRef"
|
||||
:modelValue="previewDialogVisible"
|
||||
@update:modelValue="previewDialogVisible = $event"
|
||||
:user="currentUser"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||
import { getUserList, deleteUser } from "@/api/babyhealth";
|
||||
import UserEditDialog from "./components/userEdit.vue";
|
||||
import ChangePasswordDialog from "./components/changePassword.vue";
|
||||
import PreviewDialog from "./components/preview.vue";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
account: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
qq: string;
|
||||
sex: number;
|
||||
group_id: number;
|
||||
status: number;
|
||||
last_login_ip: string;
|
||||
email: number;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
id: number;
|
||||
name: string;
|
||||
status?: number;
|
||||
rights?: string;
|
||||
create_time?: string;
|
||||
update_time?: string;
|
||||
}
|
||||
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
const users = ref<User[]>([]);
|
||||
const roles = ref<Role[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 组件引用
|
||||
const userEditRef = ref();
|
||||
const changePasswordRef = ref();
|
||||
const previewDialogRef = ref();
|
||||
|
||||
// 编辑/密码对话框状态
|
||||
const editDialogVisible = ref(false);
|
||||
const passwordDialogVisible = ref(false);
|
||||
const previewDialogVisible = ref(false);
|
||||
const editDialogTitle = ref("添加用户");
|
||||
const isEdit = ref(false);
|
||||
const currentUserId = ref<number | undefined>(undefined);
|
||||
const currentUser = ref<User | undefined>(undefined);
|
||||
|
||||
//刷新
|
||||
const refresh = async () => {
|
||||
await fetchUsers();
|
||||
};
|
||||
|
||||
// 获取角色列表
|
||||
const fetchRoles = async () => {
|
||||
try {
|
||||
const res = await getAllRoles();
|
||||
roles.value = res.data || [];
|
||||
} catch (e) {
|
||||
roles.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 获取性别tag类型
|
||||
function getSexTagType(sex: number): string {
|
||||
if (sex === 1) return 'primary'; // 男
|
||||
if (sex === 2) return 'danger'; // 女
|
||||
return 'info'; // 未知
|
||||
}
|
||||
|
||||
// 获取性别tag文本
|
||||
function getSexTagText(sex: number): string {
|
||||
if (sex === 1) return '男';
|
||||
if (sex === 2) return '女';
|
||||
return '未知';
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getUserList();
|
||||
users.value = res.data || [];
|
||||
} catch (e) {
|
||||
users.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加用户
|
||||
const handleAddUser = () => {
|
||||
isEdit.value = false;
|
||||
editDialogVisible.value = true;
|
||||
if (userEditRef.value) {
|
||||
userEditRef.value.open();
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑用户
|
||||
const handleEdit = (user: User) => {
|
||||
isEdit.value = true;
|
||||
editDialogVisible.value = true;
|
||||
if (userEditRef.value) {
|
||||
userEditRef.value.open(user);
|
||||
}
|
||||
};
|
||||
|
||||
// 预览用户
|
||||
const handlePreview = (user: User) => {
|
||||
currentUser.value = user;
|
||||
previewDialogVisible.value = true;
|
||||
if (previewDialogRef.value) {
|
||||
previewDialogRef.value.open(user);
|
||||
}
|
||||
};
|
||||
|
||||
//修改密码
|
||||
const handleChangePassword = async (user: User) => {
|
||||
changePasswordRef.value.open(user.id, user.account);
|
||||
passwordDialogVisible.value = true;
|
||||
currentUserId.value = user.id;
|
||||
};
|
||||
|
||||
// 编辑成功回调
|
||||
const handleEditSuccess = () => {
|
||||
editDialogVisible.value = false;
|
||||
ElMessage.success(isEdit.value ? "编辑成功" : "添加成功");
|
||||
fetchUsers();
|
||||
};
|
||||
|
||||
// 密码修改成功回调
|
||||
const handlePasswordChangeSuccess = () => {
|
||||
passwordDialogVisible.value = false;
|
||||
ElMessage.success("密码修改成功");
|
||||
};
|
||||
|
||||
// 分页改变
|
||||
const handlePageChange = (val: number) => {
|
||||
page.value = val;
|
||||
fetchUsers();
|
||||
};
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = async (user: User) => {
|
||||
ElMessageBox.confirm("确认删除该用户?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteUser(user.id);
|
||||
ElMessage.success("删除成功");
|
||||
fetchUsers();
|
||||
} catch (e) {
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
fetchUsers();
|
||||
fetchRoles();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-alert__title) {
|
||||
color: #f56c6c !important;
|
||||
}
|
||||
|
||||
.name-link {
|
||||
color: #3973ff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -134,7 +134,7 @@ const fetchRoles = async () => {
|
||||
const res = await getAllRoles();
|
||||
if (res.code === 200) {
|
||||
roles.value = res.data || [];
|
||||
console.log('角色列表:', roles.value);
|
||||
// console.log('角色列表:', roles.value);
|
||||
} else {
|
||||
error.value = res.msg || "获取角色列表失败";
|
||||
ElMessage.error(error.value);
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
</div>
|
||||
<div class="group-count">{{ group.total }} 个文件</div>
|
||||
</div>
|
||||
<div class="group-actions" v-if="group.id !== 0">
|
||||
<div class="group-actions" v-if="group.id !== 0 && group.id !== 999">
|
||||
<div class="action-buttons" @click.stop>
|
||||
<el-button
|
||||
type="primary"
|
||||
@ -500,7 +500,7 @@ const getFileUrl = (url: string) => {
|
||||
return `${import.meta.env.VITE_API_BASE_URL}${url}`;
|
||||
};
|
||||
|
||||
// 未分类分组(固定,不可编辑)
|
||||
//未分类分组(固定,不可编辑)
|
||||
const uncategorizedGroup = ref<Group>({
|
||||
id: 0,
|
||||
name: "未分类",
|
||||
@ -508,9 +508,17 @@ const uncategorizedGroup = ref<Group>({
|
||||
description: "未分类的文件",
|
||||
});
|
||||
|
||||
// 头像分组(固定,不可编辑)
|
||||
const avatarGroup = ref<Group>({
|
||||
id: 999,
|
||||
name: "头像",
|
||||
total: 0,
|
||||
description: "用户头像",
|
||||
});
|
||||
|
||||
// 过滤后的分组列表(未分类组置顶)
|
||||
const filteredGroups = computed(() => {
|
||||
let result = [uncategorizedGroup.value, ...groups.value];
|
||||
let result = [uncategorizedGroup.value, avatarGroup.value, ...groups.value];
|
||||
|
||||
if (groupSearchQuery.value) {
|
||||
const query = groupSearchQuery.value.toLowerCase();
|
||||
@ -643,6 +651,9 @@ const loadFiles = async () => {
|
||||
if (selectedGroup.value.id === 0) {
|
||||
// 更新未分类组的文件数量
|
||||
uncategorizedGroup.value.total = res.data.total || 0;
|
||||
} else if (selectedGroup.value.id === 999) {
|
||||
// 更新头像组的文件数量
|
||||
avatarGroup.value.total = res.data.total || 0;
|
||||
} else {
|
||||
// 更新普通分组的文件数量
|
||||
const group = groups.value.find(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user