更新结构
This commit is contained in:
parent
90b290af1f
commit
c4b24bfc05
@ -3,7 +3,7 @@ import request from "@/utils/request";
|
|||||||
// 登录(使用租户名称)
|
// 登录(使用租户名称)
|
||||||
export function login(data) {
|
export function login(data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/login`,
|
url: `/backend/login`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@ -12,7 +12,7 @@ export function login(data) {
|
|||||||
// 发送登录验证码(手机号)
|
// 发送登录验证码(手机号)
|
||||||
export function sendLoginCode(data) {
|
export function sendLoginCode(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/sendLoginCode",
|
url: "/backend/sendLoginCode",
|
||||||
method: "post",
|
method: "post",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@ -21,7 +21,7 @@ export function sendLoginCode(data) {
|
|||||||
// 手机号验证码登录
|
// 手机号验证码登录
|
||||||
export function loginBySms(data) {
|
export function loginBySms(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/loginBySms",
|
url: "/backend/loginBySms",
|
||||||
method: "post",
|
method: "post",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@ -41,7 +41,7 @@ export function logout(userInfo = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/logout`,
|
url: `/backend/logout`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: userInfo ? { userInfo: userInfo } : {},
|
data: userInfo ? { userInfo: userInfo } : {},
|
||||||
});
|
});
|
||||||
@ -53,7 +53,7 @@ export function logout(userInfo = null) {
|
|||||||
*/
|
*/
|
||||||
export function getGeetest3Infos() {
|
export function getGeetest3Infos() {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/login/getGeetest3Infos',
|
url: '/backend/login/getGeetest3Infos',
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -64,7 +64,7 @@ export function getGeetest3Infos() {
|
|||||||
*/
|
*/
|
||||||
export function getGeetest4Infos() {
|
export function getGeetest4Infos() {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/login/getGeetest4Infos',
|
url: '/backend/login/getGeetest4Infos',
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -75,7 +75,7 @@ export function getGeetest4Infos() {
|
|||||||
*/
|
*/
|
||||||
export function getOpenVerify() {
|
export function getOpenVerify() {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/login/getOpenVerify',
|
url: '/backend/login/getOpenVerify',
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@ export function getOpenVerify() {
|
|||||||
// 注册
|
// 注册
|
||||||
export function register(data) {
|
export function register(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/register",
|
url: "/backend/register",
|
||||||
method: "post",
|
method: "post",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@ -92,7 +92,7 @@ export function register(data) {
|
|||||||
// 发送注册验证码
|
// 发送注册验证码
|
||||||
export function sendRegisterCode(data) {
|
export function sendRegisterCode(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/sendRegisterCode",
|
url: "/backend/sendRegisterCode",
|
||||||
method: "post",
|
method: "post",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@ -101,7 +101,7 @@ export function sendRegisterCode(data) {
|
|||||||
// 忘记密码重置
|
// 忘记密码重置
|
||||||
export function resetPassword(data) {
|
export function resetPassword(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/resetPassword",
|
url: "/backend/resetPassword",
|
||||||
method: "post",
|
method: "post",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@ -110,7 +110,7 @@ export function resetPassword(data) {
|
|||||||
// 发送找回密码验证码
|
// 发送找回密码验证码
|
||||||
export function sendResetCode(data) {
|
export function sendResetCode(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/sendResetCode",
|
url: "/backend/sendResetCode",
|
||||||
method: "post",
|
method: "post",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,369 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog
|
|
||||||
v-model="dialogVisible"
|
|
||||||
title="绑定父母"
|
|
||||||
width="500px"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
@close="handleClose"
|
|
||||||
>
|
|
||||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
|
|
||||||
|
|
||||||
<el-form-item label="父亲">
|
|
||||||
<div class="parent-selector">
|
|
||||||
<el-select
|
|
||||||
v-model="formData.father_id"
|
|
||||||
placeholder="请输入手机号或者账号"
|
|
||||||
clearable
|
|
||||||
filterable
|
|
||||||
remote
|
|
||||||
reserve-keyword
|
|
||||||
:remote-method="(query) => handleSearchUser(query, 1)"
|
|
||||||
:loading="searchLoading"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<template #default>
|
|
||||||
<div class="search-tip">支持账号/手机号搜索</div>
|
|
||||||
<el-option
|
|
||||||
v-for="user in maleUsers"
|
|
||||||
:key="user.id"
|
|
||||||
:label="user.name || user.account"
|
|
||||||
:value="user.id"
|
|
||||||
>
|
|
||||||
<div class="user-option">
|
|
||||||
<span class="user-name">{{ user.name || user.account }}</span>
|
|
||||||
<span class="user-phone">{{ user.phone }}</span>
|
|
||||||
</div>
|
|
||||||
</el-option>
|
|
||||||
</template>
|
|
||||||
</el-select>
|
|
||||||
<el-button link type="primary" @click="handleSearchUser('', 1)">
|
|
||||||
<i class="fa-solid fa-refresh"></i> 刷新列表
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="母亲">
|
|
||||||
<div class="parent-selector">
|
|
||||||
<el-select
|
|
||||||
v-model="formData.mother_id"
|
|
||||||
placeholder="请输入手机号或者账号"
|
|
||||||
clearable
|
|
||||||
filterable
|
|
||||||
remote
|
|
||||||
reserve-keyword
|
|
||||||
:remote-method="(query) => handleSearchUser(query, 2)"
|
|
||||||
:loading="searchLoading"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<template #default>
|
|
||||||
<div class="search-tip">支持账号/手机号搜索</div>
|
|
||||||
<el-option
|
|
||||||
v-for="user in femaleUsers"
|
|
||||||
:key="user.id"
|
|
||||||
:label="user.name || user.account"
|
|
||||||
:value="user.id"
|
|
||||||
>
|
|
||||||
<div class="user-option">
|
|
||||||
<span class="user-name">{{ user.name || user.account }}</span>
|
|
||||||
<span class="user-phone">{{ user.phone }}</span>
|
|
||||||
</div>
|
|
||||||
</el-option>
|
|
||||||
</template>
|
|
||||||
</el-select>
|
|
||||||
<el-button link type="primary" @click="handleSearchUser('', 2)">
|
|
||||||
<i class="fa-solid fa-refresh"></i> 刷新列表
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, reactive, watch, computed } from 'vue';
|
|
||||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
|
||||||
import { bindParent, getParents, getUserList } from '@/api/babyhealth';
|
|
||||||
|
|
||||||
interface Baby {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
nickname: string;
|
|
||||||
avatar: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
account: string;
|
|
||||||
phone: string;
|
|
||||||
sex: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
visible: boolean;
|
|
||||||
babyData?: Baby | null;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:visible': [value: boolean];
|
|
||||||
success: [];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const dialogVisible = ref(false);
|
|
||||||
const loading = ref(false);
|
|
||||||
const searchLoading = ref(false);
|
|
||||||
const formRef = ref<FormInstance>();
|
|
||||||
const users = ref<User[]>([]);
|
|
||||||
const allUsers = ref<User[]>([]);
|
|
||||||
const parents = ref<User[]>([]);
|
|
||||||
|
|
||||||
const formData = reactive<{
|
|
||||||
father_id: number | null;
|
|
||||||
mother_id: number | null;
|
|
||||||
}>({
|
|
||||||
father_id: null,
|
|
||||||
mother_id: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const formRules: FormRules = {};
|
|
||||||
|
|
||||||
// 过滤男性用户
|
|
||||||
const maleUsers = computed(() => {
|
|
||||||
return users.value.filter(user => user.sex === 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 过滤女性用户
|
|
||||||
const femaleUsers = computed(() => {
|
|
||||||
return users.value.filter(user => user.sex === 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 拼接接口路径
|
|
||||||
const getEnvUrl = (path: string) => {
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
|
||||||
return `${API_BASE_URL}${path}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
const resetForm = () => {
|
|
||||||
formData.father_id = null;
|
|
||||||
formData.mother_id = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭对话框
|
|
||||||
const handleClose = () => {
|
|
||||||
dialogVisible.value = false;
|
|
||||||
resetForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听 visible 变化
|
|
||||||
watch(() => props.visible, (val) => {
|
|
||||||
dialogVisible.value = val;
|
|
||||||
if (val) {
|
|
||||||
fetchUsers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听对话框关闭,重置搜索结果
|
|
||||||
watch(dialogVisible, (val) => {
|
|
||||||
if (!val) {
|
|
||||||
users.value = parents.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听 dialogVisible 变化
|
|
||||||
watch(dialogVisible, (val) => {
|
|
||||||
emit('update:visible', val);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取用户列表和已有父母数据
|
|
||||||
const fetchUsers = async () => {
|
|
||||||
try {
|
|
||||||
// 获取所有用户列表
|
|
||||||
const res = await getUserList();
|
|
||||||
if (res.code === 200) {
|
|
||||||
allUsers.value = res.data || [];
|
|
||||||
users.value = res.data || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前宝贝已绑定的父母数据
|
|
||||||
if (props.babyData?.id) {
|
|
||||||
const parentsRes = await getParents(props.babyData.id);
|
|
||||||
if (parentsRes.code === 200 && parentsRes.data) {
|
|
||||||
formData.father_id = parentsRes.data.father_id || null;
|
|
||||||
formData.mother_id = parentsRes.data.mother_id || null;
|
|
||||||
|
|
||||||
// 如果已绑定父亲但不在用户列表中,添加进去以便显示名称
|
|
||||||
if (parentsRes.data.father_id && parentsRes.data.father) {
|
|
||||||
const fatherExists = allUsers.value.some(u => u.id === parentsRes.data.father_id);
|
|
||||||
if (!fatherExists) {
|
|
||||||
allUsers.value.push({
|
|
||||||
id: parentsRes.data.father_id,
|
|
||||||
name: parentsRes.data.father,
|
|
||||||
account: '',
|
|
||||||
phone: '',
|
|
||||||
sex: 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果已绑定母亲但不在用户列表中,添加进去以便显示名称
|
|
||||||
if (parentsRes.data.mother_id && parentsRes.data.mother) {
|
|
||||||
const motherExists = allUsers.value.some(u => u.id === parentsRes.data.mother_id);
|
|
||||||
if (!motherExists) {
|
|
||||||
allUsers.value.push({
|
|
||||||
id: parentsRes.data.mother_id,
|
|
||||||
name: parentsRes.data.mother,
|
|
||||||
account: '',
|
|
||||||
phone: '',
|
|
||||||
sex: 2
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户列表
|
|
||||||
users.value = allUsers.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取数据失败:', error);
|
|
||||||
ElMessage.error('获取数据失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 搜索用户
|
|
||||||
const handleSearchUser = async (query: string, sex: number) => {
|
|
||||||
searchLoading.value = true;
|
|
||||||
try {
|
|
||||||
if (!query) {
|
|
||||||
// 如果查询为空,显示所有对应性别的用户
|
|
||||||
users.value = allUsers.value.filter(user => user.sex === sex);
|
|
||||||
} else {
|
|
||||||
// 通过账号或手机号搜索
|
|
||||||
const filteredUsers = allUsers.value.filter(user => {
|
|
||||||
const isTargetSex = user.sex === sex;
|
|
||||||
const matchAccount = user.account?.toLowerCase().includes(query.toLowerCase());
|
|
||||||
const matchPhone = user.phone?.includes(query);
|
|
||||||
const matchName = user.name?.toLowerCase().includes(query.toLowerCase());
|
|
||||||
return isTargetSex && (matchAccount || matchPhone || matchName);
|
|
||||||
});
|
|
||||||
users.value = filteredUsers;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('搜索用户失败:', error);
|
|
||||||
ElMessage.error('搜索用户失败');
|
|
||||||
} finally {
|
|
||||||
searchLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!formRef.value) return;
|
|
||||||
if (!props.babyData?.id) {
|
|
||||||
ElMessage.error('宝贝信息不存在');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const submitData: Record<string, any> = {};
|
|
||||||
if (formData.father_id) {
|
|
||||||
submitData.father_id = formData.father_id;
|
|
||||||
}
|
|
||||||
if (formData.mother_id) {
|
|
||||||
submitData.mother_id = formData.mother_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await bindParent(props.babyData.id, submitData);
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success('绑定成功');
|
|
||||||
dialogVisible.value = false;
|
|
||||||
resetForm();
|
|
||||||
emit('success');
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || '绑定失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('绑定失败:', error);
|
|
||||||
ElMessage.error('绑定失败');
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.baby-info-display {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px;
|
|
||||||
background: var(--el-fill-color-light);
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
.baby-avatar-mini {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.baby-info-text {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.baby-name {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.baby-nickname {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.parent-selector {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.el-select {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-tip {
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
||||||
background: var(--el-fill-color-lighter);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-option {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.user-name {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-phone {
|
|
||||||
margin-left: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,428 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑宝贝' : '添加宝贝'" width="600px" :close-on-click-modal="false"
|
|
||||||
@close="handleClose">
|
|
||||||
<div class="dialog-content">
|
|
||||||
|
|
||||||
<!-- 表单区域 -->
|
|
||||||
<el-form ref="editFormRef" :model="formData" :rules="formRules" label-width="100px">
|
|
||||||
<el-form-item label="头像" prop="avatar">
|
|
||||||
<div class="preview-image">
|
|
||||||
<img v-if="formData.avatar" :src="getEnvUrl(formData.avatar)" class="preview-img" />
|
|
||||||
<div v-else class="no-preview">
|
|
||||||
<el-icon>
|
|
||||||
<Picture />
|
|
||||||
</el-icon>
|
|
||||||
<span>头像预览</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 裁剪区域 -->
|
|
||||||
<div class="cutter-container">
|
|
||||||
<img-cutter ref="imgCutterRef" :img="formData.avatar ? getEnvUrl(formData.avatar) : ''" :cutWidth="400"
|
|
||||||
:cutHeight="400" :rate="'1:1'" :fixedNumber="[1, 1]" :fixed="true" :fixedBox="true" :original="false"
|
|
||||||
:autoCrop="true" :autoCropWidth="300" :autoCropHeight="300" :centerBox="true" :showChooseBtn="true"
|
|
||||||
:boxWidth="300" :boxHeight="300" @cutDown="handleCut" class="img-cutter-wrapper">
|
|
||||||
<template #cut>
|
|
||||||
<div style="padding: 20px; text-align: center;">
|
|
||||||
<el-button type="primary" size="small" @click="handleCropClick">确认裁剪</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #placeholder>
|
|
||||||
<div class="avatar-uploader">
|
|
||||||
<el-icon class="avatar-uploader-icon">
|
|
||||||
<Plus />
|
|
||||||
</el-icon>
|
|
||||||
<span class="upload-text">点击上传头像</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</img-cutter>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<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 v-model="formData.height" :min="0" :max="200" :precision="1" :step="0.5" controls-position="right"
|
|
||||||
style="width: 100%">
|
|
||||||
<template #append>CM</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="体重" prop="weight">
|
|
||||||
<el-input v-model="formData.weight" :min="0" :max="50" :precision="2" :step="0.01" controls-position="right"
|
|
||||||
style="width: 100%">
|
|
||||||
<template #append>KG</template>
|
|
||||||
</el-input>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="handleClose">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- <ImgCutter :cutWidth="300" :cutHeight="300" :tool="true" V-on:cutDown="handleCut" /> -->
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, reactive, watch, nextTick } from 'vue';
|
|
||||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
|
||||||
import { Plus, Picture } from '@element-plus/icons-vue';
|
|
||||||
// import { createBaby, editBaby } from '@/api/babyhealth';
|
|
||||||
// import { uploadAvatar } from '@/api/file';
|
|
||||||
|
|
||||||
import ImgCutter from 'vue-img-cutter';
|
|
||||||
|
|
||||||
// 定义 ImgCutter 实例类型
|
|
||||||
interface ImgCutterInstance {
|
|
||||||
chooseImg: () => void;
|
|
||||||
resetCrop: () => void;
|
|
||||||
crop: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义裁剪数据结构类型
|
|
||||||
interface CroppedData {
|
|
||||||
blob: Blob;
|
|
||||||
dataURL: string;
|
|
||||||
file: File;
|
|
||||||
fileName: string;
|
|
||||||
index: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BabyFormData {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
nickname: string;
|
|
||||||
sex: number;
|
|
||||||
birth: string;
|
|
||||||
height: number;
|
|
||||||
weight: number;
|
|
||||||
avatar: string;
|
|
||||||
status: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const imgCutterRef = ref<ImgCutterInstance | null>(null);
|
|
||||||
|
|
||||||
// 处理裁剪按钮点击
|
|
||||||
const handleCropClick = () => {
|
|
||||||
if (imgCutterRef.value) {
|
|
||||||
(imgCutterRef.value as any).crop();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
visible: boolean;
|
|
||||||
editData?: BabyFormData;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:visible': [value: boolean];
|
|
||||||
success: [];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const dialogVisible = ref(false);
|
|
||||||
const loading = ref(false);
|
|
||||||
const editFormRef = ref<FormInstance>();
|
|
||||||
const cropDialogVisible = ref(false);
|
|
||||||
const croppingImage = ref('');
|
|
||||||
let selectedFile: File | null = null;
|
|
||||||
// 图片大小限制 (5MB)
|
|
||||||
const MAX_IMAGE_SIZE = 5 * 1024 * 1024;
|
|
||||||
|
|
||||||
// 支持的图片类型
|
|
||||||
const SUPPORTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
|
|
||||||
|
|
||||||
// 处理裁剪完成
|
|
||||||
const handleCut = async (data: CroppedData) => {
|
|
||||||
try {
|
|
||||||
// 创建FormData
|
|
||||||
const uploadFormData = new FormData();
|
|
||||||
uploadFormData.append('file', data.file);
|
|
||||||
uploadFormData.append('cate', 'baby_avatar');
|
|
||||||
|
|
||||||
// 上传裁剪后的图片
|
|
||||||
const response = await uploadAvatar(uploadFormData);
|
|
||||||
|
|
||||||
if (response.code === 200 || response.code === 201) {
|
|
||||||
const avatarUrl = response.data.url || response.data.path || '';
|
|
||||||
formData.avatar = avatarUrl;
|
|
||||||
|
|
||||||
if (response.code === 201) {
|
|
||||||
ElMessage.info("文件已存在,使用已有图片");
|
|
||||||
} else {
|
|
||||||
ElMessage.success("头像上传成功");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ElMessage.error(response.msg || "上传失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('上传失败:', error);
|
|
||||||
ElMessage.error("上传失败,请重试");
|
|
||||||
} finally {
|
|
||||||
// 清理
|
|
||||||
croppingImage.value = '';
|
|
||||||
selectedFile = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭对话框
|
|
||||||
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);
|
|
||||||
} else {
|
|
||||||
isEdit.value = false;
|
|
||||||
resetForm();
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!editFormRef.value) return;
|
|
||||||
|
|
||||||
await editFormRef.value.validate(async (valid) => {
|
|
||||||
if (valid) {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
let res;
|
|
||||||
if (formData.id) {
|
|
||||||
// 编辑
|
|
||||||
const submitData: Record<string, any> = {};
|
|
||||||
Object.keys(formData).forEach(key => {
|
|
||||||
if (key !== 'id' && formData[key as keyof BabyFormData] !== undefined && formData[key as keyof BabyFormData] !== null) {
|
|
||||||
submitData[key] = formData[key as keyof BabyFormData];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
res = await editBaby(formData.id, submitData);
|
|
||||||
} else {
|
|
||||||
// 新增
|
|
||||||
const submitData = new FormData();
|
|
||||||
Object.keys(formData).forEach(key => {
|
|
||||||
if (key !== 'id' && formData[key as keyof BabyFormData] !== undefined && formData[key as keyof BabyFormData] !== null) {
|
|
||||||
submitData.append(key, String(formData[key as keyof BabyFormData]));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
res = await createBaby(submitData);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success(formData.id ? '编辑成功' : '添加成功');
|
|
||||||
dialogVisible.value = false;
|
|
||||||
resetForm();
|
|
||||||
emit('success');
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || (formData.id ? '编辑失败' : '添加失败'));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('提交失败:', error);
|
|
||||||
ElMessage.error('提交失败');
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.dialog-content {
|
|
||||||
padding: 20px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-image {
|
|
||||||
width: 168px;
|
|
||||||
height: 168px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fff;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px dashed var(--el-border-color);
|
|
||||||
margin-right: 30px;
|
|
||||||
|
|
||||||
.preview-img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-preview {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: var(--el-text-color-placeholder);
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-section {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
.cutter-container {
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.avatar-uploader) {
|
|
||||||
border: 1px dashed var(--el-border-color);
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: var(--el-transition-duration-fast);
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--el-fill-color-light);
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--el-color-primary);
|
|
||||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-uploader-icon {
|
|
||||||
font-size: 32px;
|
|
||||||
color: var(--el-text-color-placeholder);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-text {
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-cutter-wrapper {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
:deep(.img-cutter) {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
max-width: none;
|
|
||||||
|
|
||||||
.cut-btn {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cut-box {
|
|
||||||
width: 100% !important;
|
|
||||||
height: 350px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cut-container {
|
|
||||||
width: 100% !important;
|
|
||||||
height: 350px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cut-preview-box {
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,400 +0,0 @@
|
|||||||
<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="handleBindParents(baby)">
|
|
||||||
<i class="fa-solid fa-link"></i>
|
|
||||||
</el-button>
|
|
||||||
<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"
|
|
||||||
@success="handleEditSuccess"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 绑定父母对话框 -->
|
|
||||||
<BindParents
|
|
||||||
v-model:visible="bindParentsVisible"
|
|
||||||
:babyData="currentBaby"
|
|
||||||
@success="handleBindParentsSuccess"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
import BabyEdit from './components/edit.vue';
|
|
||||||
import BindParents from './components/bindParents.vue';
|
|
||||||
import { getBabyList, deleteBaby } 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 bindParentsVisible = 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 deleteBaby(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 handleBindParents = (row: Baby) => {
|
|
||||||
currentBaby.value = row;
|
|
||||||
bindParentsVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 编辑/添加成功回调
|
|
||||||
const handleEditSuccess = () => {
|
|
||||||
fetchBabyList();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 绑定父母成功回调
|
|
||||||
const handleBindParentsSuccess = () => {
|
|
||||||
fetchBabyList();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取宝贝列表
|
|
||||||
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="less">
|
|
||||||
.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>
|
|
||||||
@ -1,293 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="statistics-container">
|
|
||||||
<!-- 第一行:宝贝统计 -->
|
|
||||||
<el-row :gutter="20" class="data-overview">
|
|
||||||
<el-col :span="8" v-for="item in babyData" :key="item.title">
|
|
||||||
<el-card shadow="hover" class="data-card">
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="icon-box" :style="{ backgroundColor: item.color }">
|
|
||||||
<i :class="item.icon"></i>
|
|
||||||
</div>
|
|
||||||
<div class="text-box">
|
|
||||||
<div class="title">{{ item.title }}</div>
|
|
||||||
<div class="value">{{ item.value.toLocaleString() }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<!-- 第二行:用户统计 -->
|
|
||||||
<el-row :gutter="20" class="data-overview" style="margin-top: 20px;">
|
|
||||||
<el-col :span="8" v-for="item in userData" :key="item.title">
|
|
||||||
<el-card shadow="hover" class="data-card">
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="icon-box" :style="{ backgroundColor: item.color }">
|
|
||||||
<i :class="item.icon"></i>
|
|
||||||
</div>
|
|
||||||
<div class="text-box">
|
|
||||||
<div class="title">{{ item.title }}</div>
|
|
||||||
<div class="value">{{ item.value.toLocaleString() }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<!-- 图表区域 -->
|
|
||||||
<el-row :gutter="20" class="charts-row" style="margin-top: 20px;">
|
|
||||||
<!-- 宝贝增长趋势柱状图 -->
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-card shadow="hover" header="宝贝增长趋势">
|
|
||||||
<div ref="babyChartRef" class="chart-box"></div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<!-- 用户增长趋势柱状图 -->
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-card shadow="hover" header="用户增长趋势">
|
|
||||||
<div ref="userChartRef" class="chart-box"></div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted, onUnmounted, shallowRef, nextTick } from 'vue';
|
|
||||||
import * as echarts from 'echarts';
|
|
||||||
import { getDashborad } from '@/api/babyhealth';
|
|
||||||
|
|
||||||
// --- 类型定义 ---
|
|
||||||
interface SummaryItem {
|
|
||||||
title: string;
|
|
||||||
value: number;
|
|
||||||
icon: string;
|
|
||||||
color: string;
|
|
||||||
percentage: number;
|
|
||||||
isUp: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 响应式数据 ---
|
|
||||||
const babyChartRef = ref<HTMLElement | null>(null);
|
|
||||||
const userChartRef = ref<HTMLElement | null>(null);
|
|
||||||
const babyChartInstance = shallowRef<echarts.ECharts | null>(null);
|
|
||||||
const userChartInstance = shallowRef<echarts.ECharts | null>(null);
|
|
||||||
|
|
||||||
const babyData = ref<SummaryItem[]>([
|
|
||||||
{ title: '总宝贝数', value: 0, icon: 'fa-solid fa-baby', color: '#67C23A', percentage: 0, isUp: false },
|
|
||||||
{ title: '男宝宝数', value: 0, icon: 'fa-solid fa-person', color: '#409EFF', percentage: 0, isUp: false },
|
|
||||||
{ title: '女宝宝数', value: 0, icon: 'fa-solid fa-person-dress', color: '#F56C6C', percentage: 0, isUp: false }
|
|
||||||
]);
|
|
||||||
|
|
||||||
const userData = ref<SummaryItem[]>([
|
|
||||||
{ title: '总用户数', value: 0, icon: 'fa-solid fa-users', color: '#3973FF', percentage: 0, isUp: false },
|
|
||||||
{ title: '父亲数', value: 0, icon: 'fa-solid fa-person', color: '#409EFF', percentage: 0, isUp: false },
|
|
||||||
{ title: '母亲数', value: 0, icon: 'fa-solid fa-person-dress', color: '#F56C6C', percentage: 0, isUp: false }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 调用总输出接口
|
|
||||||
async function fetchGetDashborad() {
|
|
||||||
try {
|
|
||||||
const res = await getDashborad();
|
|
||||||
if (res.code === 200 && res.data) {
|
|
||||||
const { userCounts, babyCounts } = res.data;
|
|
||||||
|
|
||||||
// 更新宝贝数据
|
|
||||||
if (babyCounts) {
|
|
||||||
babyData.value[0].value = babyCounts.total || 0; // 总宝贝数
|
|
||||||
babyData.value[1].value = babyCounts.male || 0; // 男宝宝数
|
|
||||||
babyData.value[2].value = babyCounts.female || 0; // 女宝宝数
|
|
||||||
|
|
||||||
// 更新宝贝图表
|
|
||||||
updateBabyChart(
|
|
||||||
babyCounts.total || 0,
|
|
||||||
babyCounts.male || 0,
|
|
||||||
babyCounts.female || 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户数据
|
|
||||||
if (userCounts) {
|
|
||||||
userData.value[0].value = userCounts.total || 0; // 总用户数
|
|
||||||
userData.value[1].value = userCounts.father || 0; // 父亲数
|
|
||||||
userData.value[2].value = userCounts.mother || 0; // 母亲数
|
|
||||||
|
|
||||||
// 更新用户图表
|
|
||||||
updateUserChart(
|
|
||||||
userCounts.total || 0,
|
|
||||||
userCounts.father || 0,
|
|
||||||
userCounts.mother || 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取仪表盘数据失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 初始化宝贝柱状图
|
|
||||||
const initBabyChart = () => {
|
|
||||||
if (babyChartRef.value) {
|
|
||||||
babyChartInstance.value = echarts.init(babyChartRef.value);
|
|
||||||
babyChartInstance.value.setOption({
|
|
||||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
|
||||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: ['总宝贝数', '男宝宝数', '女宝宝数'],
|
|
||||||
axisTick: { alignWithLabel: true }
|
|
||||||
},
|
|
||||||
yAxis: { type: 'value' },
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: '数量',
|
|
||||||
type: 'bar',
|
|
||||||
barWidth: '60%',
|
|
||||||
data: [
|
|
||||||
{ value: 0, itemStyle: { color: '#67C23A' } },
|
|
||||||
{ value: 0, itemStyle: { color: '#409EFF' } },
|
|
||||||
{ value: 0, itemStyle: { color: '#F56C6C' } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化用户柱状图
|
|
||||||
const initUserChart = () => {
|
|
||||||
if (userChartRef.value) {
|
|
||||||
userChartInstance.value = echarts.init(userChartRef.value);
|
|
||||||
userChartInstance.value.setOption({
|
|
||||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
|
||||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: ['总用户数', '父亲数', '母亲数'],
|
|
||||||
axisTick: { alignWithLabel: true }
|
|
||||||
},
|
|
||||||
yAxis: { type: 'value' },
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: '数量',
|
|
||||||
type: 'bar',
|
|
||||||
barWidth: '60%',
|
|
||||||
data: [
|
|
||||||
{ value: 0, itemStyle: { color: '#3973FF' } },
|
|
||||||
{ value: 0, itemStyle: { color: '#409EFF' } },
|
|
||||||
{ value: 0, itemStyle: { color: '#F56C6C' } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新宝贝柱状图数据
|
|
||||||
const updateBabyChart = (total: number, male: number, female: number) => {
|
|
||||||
if (babyChartInstance.value) {
|
|
||||||
babyChartInstance.value.setOption({
|
|
||||||
series: [{
|
|
||||||
data: [
|
|
||||||
{ value: total, itemStyle: { color: '#67C23A' } },
|
|
||||||
{ value: male, itemStyle: { color: '#409EFF' } },
|
|
||||||
{ value: female, itemStyle: { color: '#F56C6C' } }
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新用户柱状图数据
|
|
||||||
const updateUserChart = (total: number, father: number, mother: number) => {
|
|
||||||
if (userChartInstance.value) {
|
|
||||||
userChartInstance.value.setOption({
|
|
||||||
series: [{
|
|
||||||
data: [
|
|
||||||
{ value: total, itemStyle: { color: '#3973FF' } },
|
|
||||||
{ value: father, itemStyle: { color: '#409EFF' } },
|
|
||||||
{ value: mother, itemStyle: { color: '#F56C6C' } }
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生命周期与自适应
|
|
||||||
const handleResize = () => {
|
|
||||||
babyChartInstance.value?.resize();
|
|
||||||
userChartInstance.value?.resize();
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
initBabyChart();
|
|
||||||
initUserChart();
|
|
||||||
// 等待图表初始化完成后再加载数据
|
|
||||||
await nextTick();
|
|
||||||
await fetchGetDashborad();
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', handleResize);
|
|
||||||
babyChartInstance.value?.dispose();
|
|
||||||
userChartInstance.value?.dispose();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.charts-row {
|
|
||||||
margin-top: 20px;
|
|
||||||
|
|
||||||
.chart-box {
|
|
||||||
height: 350px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<template>
|
|
||||||
<router-view />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup></script>
|
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
|
||||||
|
|
||||||
@ -1,198 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,190 +0,0 @@
|
|||||||
<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="less" 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>
|
|
||||||
@ -1,521 +0,0 @@
|
|||||||
<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" :action="uploadUrl" :headers="uploadHeaders"
|
|
||||||
:on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 裁剪对话框 -->
|
|
||||||
<CropImage ref="cropImageRef" @confirm="handleCropConfirm" />
|
|
||||||
|
|
||||||
<!-- 姓名 -->
|
|
||||||
<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/file';
|
|
||||||
|
|
||||||
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 '';
|
|
||||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}${url}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 头像上传前校验 - 只校验格式,打开裁剪对话框
|
|
||||||
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
|
||||||
const isImage = rawFile.type.startsWith('image/');
|
|
||||||
const isLt2M = rawFile.size / 1024 / 1024 < 2;
|
|
||||||
|
|
||||||
if (!isImage) {
|
|
||||||
ElMessage.error('只能上传图片文件!');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!isLt2M) {
|
|
||||||
ElMessage.error('图片大小不能超过 2MB!');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 上传文件到服务器(裁剪后调用)
|
|
||||||
const uploadFile = async (file: File) => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 使用uploadAvatar接口上传文件
|
|
||||||
const response = await uploadAvatar(formData, { cate: 'user_avatar' });
|
|
||||||
|
|
||||||
if (response.code === 200 || response.code === 201) {
|
|
||||||
// 更新用户头像
|
|
||||||
form.value.avatar = response.data.url || response.data.path || '';
|
|
||||||
ElMessage.success("头像上传成功");
|
|
||||||
} else {
|
|
||||||
ElMessage.error(response.msg || "上传失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('上传失败:', error);
|
|
||||||
ElMessage.error("上传失败,请重试");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 裁剪确认后的处理
|
|
||||||
const handleCropConfirm = async (file: File) => {
|
|
||||||
await uploadFile(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 头像上传成功
|
|
||||||
const handleAvatarSuccess = (response: any) => {
|
|
||||||
if (response.code === 200 || response.code === 201) {
|
|
||||||
form.value.avatar = response.data.url || response.data.path || '';
|
|
||||||
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="less" 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,328 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog v-model="visible" title="添加用户" width="600px" @closed="handleClosed"
|
|
||||||
destroy-on-close>
|
|
||||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px" v-loading="loading"
|
|
||||||
style="padding: 20px">
|
|
||||||
<el-form-item label="用户名" prop="account">
|
|
||||||
<el-input v-model="formData.account" placeholder="请输入用户名" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="密码" prop="password">
|
|
||||||
<el-input v-model="formData.password" type="password" placeholder="请输入密码" show-password />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="昵称" prop="name">
|
|
||||||
<el-input v-model="formData.name" placeholder="请输入昵称" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="手机号" prop="phone">
|
|
||||||
<el-input v-model="formData.phone" placeholder="请输入手机号" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="邮箱" prop="email">
|
|
||||||
<el-input v-model="formData.email" placeholder="请输入邮箱" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="状态" prop="status">
|
|
||||||
<el-radio-group v-model="formData.status">
|
|
||||||
<el-radio :label="1">启用</el-radio>
|
|
||||||
<el-radio :label="0">禁用</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="visible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="submitting" @click="submitForm">确定</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, reactive } from 'vue';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import { addUser } from '@/api/user';
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
|
||||||
const visible = ref(false);
|
|
||||||
const loading = ref(false);
|
|
||||||
const submitting = ref(false);
|
|
||||||
const formRef = ref();
|
|
||||||
const currentTenantId = ref<number | null>(null);
|
|
||||||
|
|
||||||
const formData = reactive({
|
|
||||||
account: '',
|
|
||||||
password: '',
|
|
||||||
name: '',
|
|
||||||
phone: '',
|
|
||||||
email: '',
|
|
||||||
status: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
account: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
|
||||||
password: [{ required: true, message: '请输入密码', trigger: 'blur', min: 6 }],
|
|
||||||
phone: [
|
|
||||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
email: [
|
|
||||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const open = (tenantId: number) => {
|
|
||||||
visible.value = true;
|
|
||||||
currentTenantId.value = tenantId;
|
|
||||||
Object.assign(formData, {
|
|
||||||
account: '',
|
|
||||||
password: '',
|
|
||||||
name: '',
|
|
||||||
phone: '',
|
|
||||||
email: '',
|
|
||||||
status: 1
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitForm = async () => {
|
|
||||||
if (!formRef.value) return;
|
|
||||||
|
|
||||||
await formRef.value.validate();
|
|
||||||
|
|
||||||
submitting.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const submitData = {
|
|
||||||
...formData,
|
|
||||||
tid: currentTenantId.value
|
|
||||||
};
|
|
||||||
const res = await addUser(submitData);
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success('添加成功');
|
|
||||||
visible.value = false;
|
|
||||||
emit('success');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('提交失败', error);
|
|
||||||
} finally {
|
|
||||||
submitting.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClosed = () => {
|
|
||||||
formRef.value?.resetFields();
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-drawer
|
|
||||||
v-model="visible"
|
|
||||||
title="租户详细信息"
|
|
||||||
size="600px"
|
|
||||||
@closed="handleClosed"
|
|
||||||
>
|
|
||||||
<div v-loading="loading" class="detail-container">
|
|
||||||
<el-descriptions :column="1" border>
|
|
||||||
<el-descriptions-item label="租户名称">
|
|
||||||
<span class="detail-value">{{ detailData.tenant_name }}</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="租户编码">
|
|
||||||
<el-tag type="info" effect="plain">{{ detailData.tenant_code }}</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="联系人">
|
|
||||||
{{ detailData.contact_person || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="联系电话">
|
|
||||||
{{ detailData.contact_phone || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="电子邮箱">
|
|
||||||
{{ detailData.contact_email || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="租户状态">
|
|
||||||
<el-tag :type="detailData.status === 1 ? 'success' : 'danger'">
|
|
||||||
{{ detailData.status === 1 ? '启用' : '禁用' }}
|
|
||||||
</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="租户地址">
|
|
||||||
{{ detailData.address || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="创建时间">
|
|
||||||
{{ detailData.create_time || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
|
|
||||||
<div class="footer-actions">
|
|
||||||
<el-button @click="visible = false">关闭</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-drawer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { getTenantDetail } from '@/api/tenant';
|
|
||||||
|
|
||||||
const visible = ref(false);
|
|
||||||
const loading = ref(false);
|
|
||||||
const detailData = ref<any>({});
|
|
||||||
|
|
||||||
// 暴露给父组件的方法
|
|
||||||
const open = async (id: number) => {
|
|
||||||
visible.value = true;
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const res = await getTenantDetail(id);
|
|
||||||
if (res.code === 200) {
|
|
||||||
detailData.value = res.data;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取详情失败', error);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClosed = () => {
|
|
||||||
detailData.value = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 必须暴露给外部
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.detail-container {
|
|
||||||
padding: 10px 20px;
|
|
||||||
}
|
|
||||||
.detail-value {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.footer-actions {
|
|
||||||
margin-top: 30px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
:deep(.el-descriptions__label) {
|
|
||||||
width: 120px;
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog v-model="visible" :title="formData.id ? '编辑租户' : '添加租户'" width="600px" @closed="handleClosed"
|
|
||||||
destroy-on-close>
|
|
||||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px" v-loading="loading"
|
|
||||||
style="padding: 20px">
|
|
||||||
<el-form-item label="租户名称" prop="tenant_name">
|
|
||||||
<el-input v-model="formData.tenant_name" placeholder="请输入租户名称" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="租户编码" prop="tenant_code">
|
|
||||||
<el-input v-model="formData.tenant_code" placeholder="系统自动生成" disabled />
|
|
||||||
<div class="form-tip" v-if="!formData.id" style="font-size: 12px; color: #999;">
|
|
||||||
* 编码由系统随机分配,提交时将自动校验唯一性
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="联系人" prop="contact_person">
|
|
||||||
<el-input v-model="formData.contact_person" placeholder="请输入联系人" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="联系电话" prop="contact_phone">
|
|
||||||
<el-input v-model="formData.contact_phone" placeholder="请输入联系电话" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="电子邮箱" prop="contact_email">
|
|
||||||
<el-input v-model="formData.contact_email" placeholder="请输入电子邮箱" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="租户地址" prop="address">
|
|
||||||
<el-input v-model="formData.address" type="textarea" placeholder="请输入地址" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="状态" prop="status">
|
|
||||||
<el-radio-group v-model="formData.status">
|
|
||||||
<el-radio :label="1">启用</el-radio>
|
|
||||||
<el-radio :label="0">禁用</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="visible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="submitting" @click="submitForm">确定</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, reactive } from 'vue';
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
import { createTenant, editTenant, getTenantDetail, checkTenantCode } from '@/api/tenant';
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
|
||||||
const visible = ref(false);
|
|
||||||
const loading = ref(false);
|
|
||||||
const submitting = ref(false);
|
|
||||||
const formRef = ref();
|
|
||||||
|
|
||||||
const initialData = {
|
|
||||||
id: null,
|
|
||||||
tenant_name: '',
|
|
||||||
tenant_code: '',
|
|
||||||
contact_person: '',
|
|
||||||
contact_phone: '',
|
|
||||||
contact_email: '',
|
|
||||||
address: '',
|
|
||||||
status: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
const formData = reactive({ ...initialData });
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
tenant_name: [{ required: true, message: '请输入租户名称', trigger: 'blur' }],
|
|
||||||
tenant_code: [{ required: true, message: '请输入租户编码', trigger: 'blur' }],
|
|
||||||
contact_phone: [
|
|
||||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// 暴露给父组件的方法
|
|
||||||
const open = (id?: number) => {
|
|
||||||
visible.value = true;
|
|
||||||
Object.assign(formData, initialData);
|
|
||||||
|
|
||||||
if (id) {
|
|
||||||
formData.id = id;
|
|
||||||
fetchDetail(id);
|
|
||||||
} else {
|
|
||||||
formData.tenant_code = Math.floor(100000 + Math.random() * 900000).toString();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchDetail = async (id: number) => {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const res = await getTenantDetail(id);
|
|
||||||
if (res.code === 200) {
|
|
||||||
Object.assign(formData, res.data);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitForm = async () => {
|
|
||||||
if (!formRef.value) return;
|
|
||||||
|
|
||||||
// 1. 基础表单验证(必填项等)
|
|
||||||
await formRef.value.validate();
|
|
||||||
|
|
||||||
submitting.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 2. 如果是新增模式,进入“编码唯一性”校验环
|
|
||||||
if (!formData.id) {
|
|
||||||
let isCodeValid = false;
|
|
||||||
|
|
||||||
while (!isCodeValid) {
|
|
||||||
const res = await checkTenantCode(formData.tenant_code);
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
|
||||||
// 编码可用,跳出循环
|
|
||||||
isCodeValid = true;
|
|
||||||
} else {
|
|
||||||
// 编码重复,重新生成
|
|
||||||
const newCode = Math.floor(100000 + Math.random() * 900000).toString();
|
|
||||||
|
|
||||||
// 弹出提示框告知用户
|
|
||||||
await ElMessageBox.alert(
|
|
||||||
`租户编码 [${formData.tenant_code}] 已重复,系统已自动为您重新生成为 [${newCode}],请重新点击提交。`,
|
|
||||||
'编码重复提示',
|
|
||||||
{ confirmButtonText: '我知道了', type: 'warning' }
|
|
||||||
);
|
|
||||||
|
|
||||||
formData.tenant_code = newCode;
|
|
||||||
submitting.value = false;
|
|
||||||
return; // 中断本次提交,让用户看一眼新编码后再次点击
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 发起真正的保存请求
|
|
||||||
const saveApi = formData.id ? editTenant(formData.id, formData) : createTenant(formData);
|
|
||||||
const saveRes = await saveApi;
|
|
||||||
|
|
||||||
if (saveRes.code === 200) {
|
|
||||||
ElMessage.success('保存成功');
|
|
||||||
visible.value = false;
|
|
||||||
emit('success');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('提交失败', error);
|
|
||||||
} finally {
|
|
||||||
submitting.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成 6 位随机数字并校验唯一性
|
|
||||||
*/
|
|
||||||
const generateUniqueCode = async () => {
|
|
||||||
loading.value = true;
|
|
||||||
let isUnique = false;
|
|
||||||
let newCode = '';
|
|
||||||
let retryCount = 0;
|
|
||||||
const maxRetries = 10; // 保护措施:最多重试10次
|
|
||||||
|
|
||||||
while (!isUnique && retryCount < maxRetries) {
|
|
||||||
// 1. 生成 6 位随机数字字符串
|
|
||||||
newCode = Math.floor(100000 + Math.random() * 900000).toString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 2. 调用接口校验
|
|
||||||
const res = await checkTenantCode(newCode);
|
|
||||||
if (res.code === 200) {
|
|
||||||
isUnique = true; // 接口返回 200 表示不存在,可用
|
|
||||||
} else {
|
|
||||||
console.warn(`编码 ${newCode} 重复,正在重试...`);
|
|
||||||
retryCount++;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("校验编码失败", error);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isUnique) {
|
|
||||||
formData.tenant_code = newCode;
|
|
||||||
} else {
|
|
||||||
ElMessage.error('无法生成唯一的租户编码,请重试');
|
|
||||||
}
|
|
||||||
loading.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClosed = () => {
|
|
||||||
formRef.value?.resetFields();
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog v-model="visible" title="资质文件管理" width="650px" destroy-on-close>
|
|
||||||
<el-form
|
|
||||||
ref="formRef"
|
|
||||||
:model="formData"
|
|
||||||
:rules="rules"
|
|
||||||
label-width="110px"
|
|
||||||
v-loading="loading"
|
|
||||||
style="padding: 10px 20px"
|
|
||||||
>
|
|
||||||
<el-form-item label="租户名称">
|
|
||||||
<el-input v-model="tenantName" disabled />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="资质类型" prop="type">
|
|
||||||
<el-select v-model="formData.type" placeholder="请选择资质类型" style="width: 100%">
|
|
||||||
<el-option label="营业执照" value="business_license" />
|
|
||||||
<el-option label="开户许可证" value="bank_account_permit" />
|
|
||||||
<el-option label="行业许可证" value="industry_license" />
|
|
||||||
<el-option label="其他资质" value="others" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="资质图片" prop="file_url">
|
|
||||||
<el-upload
|
|
||||||
class="avatar-uploader"
|
|
||||||
action="/api/admin/common/upload"
|
|
||||||
:show-file-list="false"
|
|
||||||
:on-success="handleUploadSuccess"
|
|
||||||
:before-upload="beforeAvatarUpload"
|
|
||||||
name="file"
|
|
||||||
>
|
|
||||||
<img v-if="formData.file_url" :src="formData.file_url" class="qualification-img" />
|
|
||||||
<el-icon v-else class="uploader-icon"><Plus /></el-icon>
|
|
||||||
</el-upload>
|
|
||||||
<div class="upload-tip">支持 jpg/png 格式,大小不超过 2MB</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="有效期至" prop="expire_time">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="formData.expire_time"
|
|
||||||
type="date"
|
|
||||||
placeholder="选择过期日期"
|
|
||||||
style="width: 100%"
|
|
||||||
value-format="YYYY-MM-DD"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="备注说明">
|
|
||||||
<el-input v-model="formData.remark" type="textarea" placeholder="请输入备注信息" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="visible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="submitting" @click="submitForm">保存资质</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, reactive } from 'vue';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import { Plus } from '@element-plus/icons-vue';
|
|
||||||
// 假设你有对应的资质接口
|
|
||||||
// import { saveQualification, getQualificationDetail } from '@/api/tenant';
|
|
||||||
|
|
||||||
const visible = ref(false);
|
|
||||||
const loading = ref(false);
|
|
||||||
const submitting = ref(false);
|
|
||||||
const tenantName = ref('');
|
|
||||||
const formRef = ref();
|
|
||||||
|
|
||||||
const formData = reactive({
|
|
||||||
tid: null,
|
|
||||||
type: '',
|
|
||||||
file_url: '',
|
|
||||||
expire_time: '',
|
|
||||||
remark: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
type: [{ required: true, message: '请选择资质类型', trigger: 'change' }],
|
|
||||||
file_url: [{ required: true, message: '请上传资质图片', trigger: 'change' }],
|
|
||||||
expire_time: [{ required: true, message: '请选择有效期', trigger: 'change' }]
|
|
||||||
};
|
|
||||||
|
|
||||||
// 打开弹窗
|
|
||||||
const open = (row: any) => {
|
|
||||||
visible.value = true;
|
|
||||||
tenantName.value = row.tenant_name;
|
|
||||||
formData.tid = row.id;
|
|
||||||
|
|
||||||
// 如果后端有资质详情接口,可以在此获取回显数据
|
|
||||||
// fetchDetail(row.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 上传前的校验
|
|
||||||
const beforeAvatarUpload = (rawFile: any) => {
|
|
||||||
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
|
|
||||||
ElMessage.error('图片必须是 JPG 或 PNG 格式!');
|
|
||||||
return false;
|
|
||||||
} else if (rawFile.size / 1024 / 1024 > 2) {
|
|
||||||
ElMessage.error('图片大小不能超过 2MB!');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 上传成功回调
|
|
||||||
const handleUploadSuccess = (response: any) => {
|
|
||||||
// 根据你后端的上传接口返回结构调整
|
|
||||||
formData.file_url = response.data.url;
|
|
||||||
ElMessage.success('上传成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitForm = async () => {
|
|
||||||
await formRef.value.validate();
|
|
||||||
submitting.value = true;
|
|
||||||
try {
|
|
||||||
// 模拟提交
|
|
||||||
console.log('提交的数据:', formData);
|
|
||||||
ElMessage.success('资质保存成功');
|
|
||||||
visible.value = false;
|
|
||||||
} finally {
|
|
||||||
submitting.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.avatar-uploader {
|
|
||||||
border: 1px dashed #d9d9d9;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 178px;
|
|
||||||
height: 178px;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
}
|
|
||||||
.avatar-uploader:hover {
|
|
||||||
border-color: #409eff;
|
|
||||||
}
|
|
||||||
.uploader-icon {
|
|
||||||
font-size: 28px;
|
|
||||||
color: #8c939d;
|
|
||||||
width: 178px;
|
|
||||||
height: 178px;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 178px !important;
|
|
||||||
}
|
|
||||||
.qualification-img {
|
|
||||||
width: 178px;
|
|
||||||
height: 178px;
|
|
||||||
display: block;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
.upload-tip {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,258 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="container-box">
|
|
||||||
<div class="header-bar">
|
|
||||||
<h2>我的域名</h2>
|
|
||||||
<el-button type="primary" @click="dialogVisible = true">
|
|
||||||
<el-icon><Plus /></el-icon>
|
|
||||||
申请二级域名
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider></el-divider>
|
|
||||||
|
|
||||||
<!-- 我的域名列表 -->
|
|
||||||
<el-table
|
|
||||||
:data="tableData"
|
|
||||||
style="width: 100%"
|
|
||||||
border
|
|
||||||
v-loading="loading"
|
|
||||||
element-loading-text="正在加载..."
|
|
||||||
>
|
|
||||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
|
||||||
<el-table-column prop="sub_domain" label="二级域名前缀" width="150" />
|
|
||||||
<el-table-column prop="main_domain" label="主域名" min-width="150" />
|
|
||||||
<el-table-column prop="full_domain" label="完整域名" min-width="200">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-link
|
|
||||||
type="primary"
|
|
||||||
:href="'http://' + scope.row.full_domain"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{{ scope.row.full_domain }}
|
|
||||||
</el-link>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-tag v-if="scope.row.status === 0" type="warning">审核中</el-tag>
|
|
||||||
<el-tag v-else-if="scope.row.status === 1" type="success"
|
|
||||||
>已生效</el-tag
|
|
||||||
>
|
|
||||||
<el-tag v-else type="danger">已禁用</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="create_time" label="申请时间" width="180" />
|
|
||||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
text
|
|
||||||
type="primary"
|
|
||||||
@click="handleCopy(scope.row.full_domain)"
|
|
||||||
>
|
|
||||||
复制
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
text
|
|
||||||
type="danger"
|
|
||||||
@click="handleDelete(scope.row)"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<!-- 申请域名弹窗 -->
|
|
||||||
<el-dialog v-model="dialogVisible" title="申请二级域名" width="500px">
|
|
||||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
|
||||||
<el-form-item label="选择主域名" prop="main_domain">
|
|
||||||
<el-select
|
|
||||||
v-model="form.main_domain"
|
|
||||||
placeholder="请选择主域名"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in domainList"
|
|
||||||
:key="item.main_domain"
|
|
||||||
:label="item.main_domain"
|
|
||||||
:value="item.main_domain"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="二级前缀" prop="sub_domain">
|
|
||||||
<el-input v-model="form.sub_domain" placeholder="请输入二级域名前缀">
|
|
||||||
<template #append>{{
|
|
||||||
form.main_domain ? "." + form.main_domain : ""
|
|
||||||
}}</template>
|
|
||||||
</el-input>
|
|
||||||
<div class="form-tip">
|
|
||||||
只能包含字母、数字和连字符,不能以连字符开头或结尾
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="预览">
|
|
||||||
<div class="domain-preview">
|
|
||||||
{{
|
|
||||||
form.sub_domain
|
|
||||||
? form.sub_domain + "." + (form.main_domain || "example.com")
|
|
||||||
: "请填写上方信息"
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading"
|
|
||||||
>提交申请</el-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, reactive, onMounted } from "vue";
|
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
|
||||||
import { Plus } from "@element-plus/icons-vue";
|
|
||||||
import {
|
|
||||||
getMyDomains,
|
|
||||||
applyTenantDomain,
|
|
||||||
getEnabledDomains,
|
|
||||||
deleteTenantDomain,
|
|
||||||
} from "@/api/domain";
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
const loading = ref(false);
|
|
||||||
const submitLoading = ref(false);
|
|
||||||
const dialogVisible = ref(false);
|
|
||||||
const formRef = ref();
|
|
||||||
|
|
||||||
const tableData = ref<any[]>([]);
|
|
||||||
const domainList = ref<any[]>([]);
|
|
||||||
|
|
||||||
const form = reactive({
|
|
||||||
main_domain: "",
|
|
||||||
sub_domain: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
main_domain: [{ required: true, message: "请选择主域名", trigger: "change" }],
|
|
||||||
sub_domain: [
|
|
||||||
{ required: true, message: "请输入二级域名前缀", trigger: "blur" },
|
|
||||||
{
|
|
||||||
pattern: /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$/,
|
|
||||||
message: "格式不正确",
|
|
||||||
trigger: "blur",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchDomains = async () => {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
// 获取我的域名列表
|
|
||||||
const res = await getMyDomains({ tid: authStore.user.tid });
|
|
||||||
if (res.code === 200) {
|
|
||||||
tableData.value = res.data || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取可选主域名
|
|
||||||
const domainRes = await getEnabledDomains();
|
|
||||||
if (domainRes.code === 200) {
|
|
||||||
domainList.value = domainRes.data || [];
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
await formRef.value.validate();
|
|
||||||
submitLoading.value = true;
|
|
||||||
try {
|
|
||||||
const res = await applyTenantDomain({
|
|
||||||
tid: authStore.user.tid,
|
|
||||||
main_domain: form.main_domain,
|
|
||||||
sub_domain: form.sub_domain,
|
|
||||||
});
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success(res.msg);
|
|
||||||
dialogVisible.value = false;
|
|
||||||
form.sub_domain = "";
|
|
||||||
fetchDomains();
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || "申请失败");
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
submitLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopy = (domain: string) => {
|
|
||||||
navigator.clipboard.writeText("http://" + domain);
|
|
||||||
ElMessage.success("链接已复制到剪贴板");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (row: any) => {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(
|
|
||||||
`确定要删除域名 "${row.full_domain}" 吗?`,
|
|
||||||
"提示",
|
|
||||||
{
|
|
||||||
confirmButtonText: "确定",
|
|
||||||
cancelButtonText: "取消",
|
|
||||||
type: "warning",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const res = await deleteTenantDomain(row.id);
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success("删除成功");
|
|
||||||
fetchDomains();
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || "删除失败");
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error !== "cancel") {
|
|
||||||
console.error("删除失败:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchDomains();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.container-box {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-tip {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.domain-preview {
|
|
||||||
padding: 10px;
|
|
||||||
background: #f5f7fa;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #409eff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,293 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="container-box">
|
|
||||||
<div class="header-bar">
|
|
||||||
<h2>租户管理</h2>
|
|
||||||
<div class="header-actions">
|
|
||||||
<el-button type="primary" @click="editRef.open()">
|
|
||||||
<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-form :inline="true" :model="searchForm" class="search-form">
|
|
||||||
<el-form-item label="租户名称">
|
|
||||||
<el-input
|
|
||||||
v-model="searchForm.tenant_name"
|
|
||||||
placeholder="请输入租户名称"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="handleSearch"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="租户编码">
|
|
||||||
<el-input
|
|
||||||
v-model="searchForm.tenant_code"
|
|
||||||
placeholder="请输入租户编码"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="handleSearch"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="联系人">
|
|
||||||
<el-input
|
|
||||||
v-model="searchForm.contact_person"
|
|
||||||
placeholder="请输入联系人"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="handleSearch"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="联系电话">
|
|
||||||
<el-input
|
|
||||||
v-model="searchForm.contact_phone"
|
|
||||||
placeholder="请输入联系电话"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="handleSearch"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
|
||||||
<el-button @click="resetSearch">重置</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<!-- 租户列表表格 -->
|
|
||||||
<el-table :data="tenants" style="width: 100%" v-loading="loading">
|
|
||||||
<el-table-column prop="id" label="ID" align="center" fixed="left" />
|
|
||||||
<el-table-column
|
|
||||||
prop="tenant_name"
|
|
||||||
label="租户名称"
|
|
||||||
min-width="220"
|
|
||||||
align="center"
|
|
||||||
/>
|
|
||||||
<el-table-column
|
|
||||||
prop="tenant_code"
|
|
||||||
label="租户编码"
|
|
||||||
min-width="120"
|
|
||||||
align="center"
|
|
||||||
/>
|
|
||||||
<el-table-column
|
|
||||||
prop="contact_person"
|
|
||||||
label="联系人"
|
|
||||||
min-width="120"
|
|
||||||
align="center"
|
|
||||||
/>
|
|
||||||
<el-table-column
|
|
||||||
prop="contact_phone"
|
|
||||||
label="联系电话"
|
|
||||||
min-width="120"
|
|
||||||
align="center"
|
|
||||||
/>
|
|
||||||
<el-table-column
|
|
||||||
prop="contact_email"
|
|
||||||
label="电子邮箱"
|
|
||||||
min-width="180"
|
|
||||||
align="center"
|
|
||||||
/>
|
|
||||||
<el-table-column
|
|
||||||
prop="address"
|
|
||||||
label="租户地址"
|
|
||||||
min-width="300"
|
|
||||||
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="280" align="center" fixed="right">
|
|
||||||
<template #default="scope">
|
|
||||||
<!-- <el-button size="small" @click="handleQualification(scope.row)">资质</el-button> -->
|
|
||||||
<el-button text size="small" @click="handleAddUser(scope.row)"
|
|
||||||
>增加用户</el-button
|
|
||||||
>
|
|
||||||
<el-button text size="small" @click="handlePreview(scope.row)"
|
|
||||||
>查看</el-button
|
|
||||||
>
|
|
||||||
<el-button text size="small" @click="editRef.open(scope.row.id)"
|
|
||||||
>编辑</el-button
|
|
||||||
>
|
|
||||||
<el-button
|
|
||||||
text
|
|
||||||
size="small"
|
|
||||||
type="danger"
|
|
||||||
@click="handleDelete(scope.row)"
|
|
||||||
>删除</el-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<EditModal ref="editRef" @success="refresh" />
|
|
||||||
<DetailDrawer ref="detailRef" />
|
|
||||||
<Qualification ref="qualificationRef" />
|
|
||||||
<AddUser ref="addUserRef" />
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<div class="pagination-bar">
|
|
||||||
<el-pagination
|
|
||||||
:current-page="page"
|
|
||||||
:page-size="pageSize"
|
|
||||||
:total="total"
|
|
||||||
@current-change="handlePageChange"
|
|
||||||
layout="total, prev, pager, next"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { getTenantList, deleteTenant } from "@/api/tenant";
|
|
||||||
import { onMounted, ref, reactive } from "vue";
|
|
||||||
import { useRouter } from "vue-router";
|
|
||||||
import EditModal from "./components/edit.vue";
|
|
||||||
import DetailDrawer from "./components/detail.vue";
|
|
||||||
import Qualification from "./components/qualification.vue";
|
|
||||||
import AddUser from "./components/adduser.vue";
|
|
||||||
|
|
||||||
const total = ref(0);
|
|
||||||
const page = ref(1);
|
|
||||||
const pageSize = ref(10);
|
|
||||||
const loading = ref(false);
|
|
||||||
const router = useRouter();
|
|
||||||
const tenants = ref([]);
|
|
||||||
|
|
||||||
const editRef = ref();
|
|
||||||
const detailRef = ref();
|
|
||||||
const addUserRef = ref();
|
|
||||||
const qualificationRef = ref();
|
|
||||||
|
|
||||||
// 删除
|
|
||||||
const handleDelete = (row) => {
|
|
||||||
ElMessageBox.confirm("确定要删除该租户吗?", "提示", {
|
|
||||||
type: "warning",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
deleteTenant(row.id).then((res) => {
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success("删除成功");
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 查看
|
|
||||||
const handlePreview = (row: any) => {
|
|
||||||
detailRef.value.open(row.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
//增加用户
|
|
||||||
const handleAddUser = (row: any) => {
|
|
||||||
addUserRef.value.open(row.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 资质
|
|
||||||
const handleQualification = (row: any) => {
|
|
||||||
qualificationRef.value.open(row.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 分页改变
|
|
||||||
const handlePageChange = (val: number) => {
|
|
||||||
page.value = val;
|
|
||||||
refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 搜索表单数据
|
|
||||||
const searchForm = reactive({
|
|
||||||
tenant_name: "",
|
|
||||||
tenant_code: "",
|
|
||||||
contact_person: "",
|
|
||||||
contact_phone: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
// 执行查询(回到第一页再刷新)
|
|
||||||
const handleSearch = () => {
|
|
||||||
page.value = 1;
|
|
||||||
refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重置查询
|
|
||||||
const resetSearch = () => {
|
|
||||||
searchForm.tenant_name = "";
|
|
||||||
searchForm.tenant_code = "";
|
|
||||||
searchForm.contact_person = "";
|
|
||||||
searchForm.contact_phone = "";
|
|
||||||
handleSearch();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 刷新
|
|
||||||
const refresh = () => {
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
// 整合分页参数和搜索参数
|
|
||||||
const queryParams = {
|
|
||||||
page: page.value,
|
|
||||||
pageSize: pageSize.value,
|
|
||||||
...searchForm,
|
|
||||||
};
|
|
||||||
|
|
||||||
getTenantList(queryParams)
|
|
||||||
.then((res) => {
|
|
||||||
if (res.code === 200) {
|
|
||||||
tenants.value = res.data.list;
|
|
||||||
total.value = res.data.total;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取租户列表
|
|
||||||
onMounted(() => {
|
|
||||||
refresh();
|
|
||||||
});
|
|
||||||
</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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-form {
|
|
||||||
background: #f9f9f9;
|
|
||||||
padding: 20px 20px 0 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,230 +0,0 @@
|
|||||||
|
|
||||||
<template>
|
|
||||||
<div class="container-box">
|
|
||||||
<div class="header-bar">
|
|
||||||
<h2>邮箱管理</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider />
|
|
||||||
|
|
||||||
<div class="content-box">
|
|
||||||
<el-form
|
|
||||||
ref="emailFormRef"
|
|
||||||
:model="emailForm"
|
|
||||||
:rules="emailRules"
|
|
||||||
label-width="120px"
|
|
||||||
class="email-form"
|
|
||||||
>
|
|
||||||
<el-form-item label="发件人邮箱" prop="fromAddress">
|
|
||||||
<el-input
|
|
||||||
v-model="emailForm.fromAddress"
|
|
||||||
placeholder="如:noreply@example.com"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="发件人名称" prop="fromName">
|
|
||||||
<el-input
|
|
||||||
v-model="emailForm.fromName"
|
|
||||||
placeholder="如:官方网站"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="SMTP 主机" prop="host">
|
|
||||||
<el-input
|
|
||||||
v-model="emailForm.host"
|
|
||||||
placeholder="如:smtp.qq.com"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="SMTP 端口" prop="port">
|
|
||||||
<el-input
|
|
||||||
v-model="emailForm.port"
|
|
||||||
placeholder="如:465 或 587"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 用户名去掉,直接使用发件人邮箱作为登录账号 -->
|
|
||||||
|
|
||||||
<el-form-item label="授权码/密码" prop="password">
|
|
||||||
<el-input
|
|
||||||
v-model="emailForm.password"
|
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
placeholder="邮箱授权码或登录密码"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="加密方式" prop="encryption">
|
|
||||||
<el-radio-group v-model="emailForm.encryption">
|
|
||||||
<el-radio label="ssl">SSL</el-radio>
|
|
||||||
<el-radio label="tls">TLS</el-radio>
|
|
||||||
<el-radio label="none">不加密</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="超时时间(秒)" prop="timeout">
|
|
||||||
<el-input
|
|
||||||
v-model="emailForm.timeout"
|
|
||||||
placeholder="默认 30 秒"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="测试收件邮箱">
|
|
||||||
<el-input
|
|
||||||
v-model="testEmail"
|
|
||||||
placeholder="输入一个邮箱地址用于测试发送"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" @click="handleSave">
|
|
||||||
保存配置
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="handleTest">
|
|
||||||
发送测试邮件
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="handleReset">重置</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { reactive, ref, onMounted } from "vue";
|
|
||||||
import { ElMessage } from "element-plus";
|
|
||||||
import type { FormInstance, FormRules } from "element-plus";
|
|
||||||
import { getEmailInfo, editEmailInfo, sendTestEmail } from "@/api/email";
|
|
||||||
|
|
||||||
const emailFormRef = ref<FormInstance>();
|
|
||||||
|
|
||||||
const emailForm = reactive({
|
|
||||||
fromAddress: "",
|
|
||||||
fromName: "",
|
|
||||||
host: "",
|
|
||||||
port: "",
|
|
||||||
password: "",
|
|
||||||
encryption: "ssl",
|
|
||||||
timeout: 30
|
|
||||||
});
|
|
||||||
|
|
||||||
const emailRules: FormRules = {
|
|
||||||
fromAddress: [
|
|
||||||
{ required: true, message: "请输入发件人邮箱", trigger: "blur" },
|
|
||||||
{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }
|
|
||||||
],
|
|
||||||
host: [{ required: true, message: "请输入 SMTP 主机", trigger: "blur" }],
|
|
||||||
port: [{ required: true, message: "请输入 SMTP 端口", trigger: "blur" }],
|
|
||||||
password: [{ required: true, message: "请输入授权码/密码", trigger: "blur" }]
|
|
||||||
};
|
|
||||||
|
|
||||||
const testEmail = ref("");
|
|
||||||
|
|
||||||
const loadEmailConfig = async () => {
|
|
||||||
const res = await getEmailInfo();
|
|
||||||
if (res.code === 200 && Array.isArray(res.data) && res.data.length > 0) {
|
|
||||||
const item = res.data[0];
|
|
||||||
emailForm.fromAddress = item.from_address || "";
|
|
||||||
emailForm.fromName = item.from_name || "";
|
|
||||||
emailForm.host = item.host || "";
|
|
||||||
emailForm.port = item.port != null ? String(item.port) : "";
|
|
||||||
emailForm.password = item.password || "";
|
|
||||||
emailForm.encryption = item.encryption || "ssl";
|
|
||||||
emailForm.timeout = item.timeout != null ? item.timeout : 30;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
if (!emailFormRef.value) return;
|
|
||||||
try {
|
|
||||||
await emailFormRef.value.validate();
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await editEmailInfo(emailForm);
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success("邮箱配置保存成功");
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || "保存失败,请稍后重试");
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || "保存失败,请稍后重试");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTest = async () => {
|
|
||||||
if (!testEmail.value) {
|
|
||||||
ElMessage.warning("请先输入测试收件邮箱");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!emailFormRef.value) return;
|
|
||||||
try {
|
|
||||||
await emailFormRef.value.validate();
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await sendTestEmail({ ...emailForm, testEmail: testEmail.value });
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success("测试邮件发送成功");
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || "测试邮件发送失败,请稍后重试");
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || "测试邮件发送失败,请稍后重试");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
emailForm.fromAddress = "";
|
|
||||||
emailForm.fromName = "";
|
|
||||||
emailForm.host = "";
|
|
||||||
emailForm.port = "";
|
|
||||||
emailForm.password = "";
|
|
||||||
emailForm.encryption = "ssl";
|
|
||||||
emailForm.timeout = 30;
|
|
||||||
testEmail.value = "";
|
|
||||||
emailFormRef.value?.clearValidate();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadEmailConfig();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.container-box {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-box {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-form {
|
|
||||||
max-width: 700px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,489 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="container-box">
|
|
||||||
<div class="header-bar">
|
|
||||||
<h2>模块管理</h2>
|
|
||||||
<div class="header-actions">
|
|
||||||
<el-button type="primary" @click="handleAdd">
|
|
||||||
<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="modules"
|
|
||||||
style="width: 100%"
|
|
||||||
v-loading="loading"
|
|
||||||
@selection-change="handleSelectionChange"
|
|
||||||
>
|
|
||||||
<el-table-column type="selection" width="55" align="center" />
|
|
||||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
|
||||||
<el-table-column prop="mid" label="MID" width="80" align="center" />
|
|
||||||
<el-table-column
|
|
||||||
prop="name"
|
|
||||||
label="模块名称"
|
|
||||||
min-width="150"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div class="module-name">
|
|
||||||
<span class="module-icon" v-html="row.icon"></span>
|
|
||||||
<span>{{ row.name }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
prop="code"
|
|
||||||
label="模块编码"
|
|
||||||
min-width="120"
|
|
||||||
align="center"
|
|
||||||
/>
|
|
||||||
<el-table-column
|
|
||||||
prop="path"
|
|
||||||
label="路由路径"
|
|
||||||
min-width="150"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag size="small" type="info">{{ row.path || "-" }}</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="type" label="分类" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag size="small" :type="row.type === 1 ? 'primary' : row.type === 2 ? 'warning' : 'info'">
|
|
||||||
{{ row.type === 1 ? '功能模块' : row.type === 2 ? '系统配置' : '未分类' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
prop="description"
|
|
||||||
label="描述"
|
|
||||||
min-width="200"
|
|
||||||
align="center"
|
|
||||||
show-overflow-tooltip
|
|
||||||
/>
|
|
||||||
<el-table-column prop="sort" label="排序" width="100" align="center" />
|
|
||||||
<el-table-column prop="is_show" label="显示" width="80" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-switch
|
|
||||||
v-model="row.is_show"
|
|
||||||
:active-value="1"
|
|
||||||
:inactive-value="0"
|
|
||||||
@change="handleShowChange(row)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
|
||||||
{{ row.status === 1 ? "启用" : "禁用" }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
|
||||||
<el-button size="small" type="danger" @click="handleDelete(row)"
|
|
||||||
>删除</el-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<div class="table-footer" v-if="selectedModules.length > 0">
|
|
||||||
<span>已选择 {{ selectedModules.length }} 项</span>
|
|
||||||
<el-button type="danger" size="small" @click="handleBatchDelete"
|
|
||||||
>批量删除</el-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider></el-divider>
|
|
||||||
|
|
||||||
<div class="tips-section">
|
|
||||||
<el-alert title="模块管理说明" type="info" :closable="false" show-icon>
|
|
||||||
<template #default>
|
|
||||||
<p>1. 模块用于管理系统功能单元,每个模块包含独立的路由、图标和描述</p>
|
|
||||||
<p>2. 模块编码用于程序识别,请确保唯一性</p>
|
|
||||||
<p>3. 禁用状态的模块将不会在菜单中显示</p>
|
|
||||||
</template>
|
|
||||||
</el-alert>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
v-model="dialogVisible"
|
|
||||||
:title="dialogType === 'add' ? '添加模块' : '编辑模块'"
|
|
||||||
width="600px"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="formRef"
|
|
||||||
:model="formData"
|
|
||||||
:rules="rules"
|
|
||||||
label-width="100px"
|
|
||||||
>
|
|
||||||
<el-form-item label="模块名称" prop="name">
|
|
||||||
<el-input v-model="formData.name" placeholder="请输入模块名称" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="模块编码" prop="code">
|
|
||||||
<el-input
|
|
||||||
v-model="formData.code"
|
|
||||||
placeholder="请输入模块编码(英文)"
|
|
||||||
:disabled="dialogType === 'edit'"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="路由路径" prop="path">
|
|
||||||
<el-input
|
|
||||||
v-model="formData.path"
|
|
||||||
placeholder="请输入路由路径,如 /system/modules"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="模块分类" prop="type">
|
|
||||||
<el-select v-model="formData.type" placeholder="请选择模块分类" style="width: 100%">
|
|
||||||
<el-option label="未分类" :value="0" />
|
|
||||||
<el-option label="功能模块" :value="1" />
|
|
||||||
<el-option label="系统配置" :value="2" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="图标" prop="icon">
|
|
||||||
<el-input
|
|
||||||
v-model="formData.icon"
|
|
||||||
placeholder="请输入图标类名,<i class='fa-solid fa-warehouse'></i>"
|
|
||||||
>
|
|
||||||
<!-- <template #append>
|
|
||||||
<el-popover placement="bottom-end" :width="400" trigger="click">
|
|
||||||
<template #reference>
|
|
||||||
<el-button link>选择图标</el-button>
|
|
||||||
</template>
|
|
||||||
<div class="icon-grid">
|
|
||||||
<div
|
|
||||||
v-for="icon in iconList"
|
|
||||||
:key="icon"
|
|
||||||
class="icon-item"
|
|
||||||
:class="{ active: formData.icon === icon }"
|
|
||||||
@click="formData.icon = icon"
|
|
||||||
>
|
|
||||||
<el-icon :size="20"><component :is="icon" /></el-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-popover>
|
|
||||||
</template> -->
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="描述" prop="description">
|
|
||||||
<el-input
|
|
||||||
v-model="formData.description"
|
|
||||||
type="textarea"
|
|
||||||
:rows="3"
|
|
||||||
placeholder="请输入模块描述"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="排序" prop="sort">
|
|
||||||
<el-input-number
|
|
||||||
v-model="formData.sort"
|
|
||||||
:min="0"
|
|
||||||
:max="999"
|
|
||||||
controls-position="right"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="是否显示" prop="is_show">
|
|
||||||
<el-switch
|
|
||||||
v-model="formData.is_show"
|
|
||||||
:active-value="1"
|
|
||||||
:inactive-value="0"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="状态" prop="status">
|
|
||||||
<el-radio-group v-model="formData.status">
|
|
||||||
<el-radio :label="1">启用</el-radio>
|
|
||||||
<el-radio :label="0">禁用</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading"
|
|
||||||
>确定</el-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, shallowRef } from "vue";
|
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
|
||||||
import {
|
|
||||||
getModulesList,
|
|
||||||
getModuleDetail,
|
|
||||||
addModule,
|
|
||||||
editModule,
|
|
||||||
deleteModule,
|
|
||||||
batchDeleteModules,
|
|
||||||
changeModuleStatus,
|
|
||||||
} from "@/api/modules";
|
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
const modules = ref([]);
|
|
||||||
const selectedModules = ref([]);
|
|
||||||
const dialogVisible = ref(false);
|
|
||||||
const dialogType = ref("add");
|
|
||||||
const formRef = ref(null);
|
|
||||||
const submitLoading = ref(false);
|
|
||||||
|
|
||||||
const formData = ref({
|
|
||||||
name: "",
|
|
||||||
code: "",
|
|
||||||
path: "",
|
|
||||||
type: 0,
|
|
||||||
icon: "",
|
|
||||||
description: "",
|
|
||||||
sort: 0,
|
|
||||||
is_show: 1,
|
|
||||||
status: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
name: [{ required: true, message: "请输入模块名称", trigger: "blur" }],
|
|
||||||
code: [{ required: true, message: "请输入模块编码", trigger: "blur" }],
|
|
||||||
};
|
|
||||||
|
|
||||||
function getIconComponent(iconName) {
|
|
||||||
return iconComponents.value[iconName] || Grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchModules() {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const res = await getModulesList();
|
|
||||||
if (res.code === 200 && res.data) {
|
|
||||||
modules.value = res.data.list || [];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取模块列表失败:", error);
|
|
||||||
ElMessage.error("获取模块列表失败");
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refresh() {
|
|
||||||
fetchModules();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSelectionChange(selection) {
|
|
||||||
selectedModules.value = selection;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAdd() {
|
|
||||||
dialogType.value = "add";
|
|
||||||
formData.value = {
|
|
||||||
name: "",
|
|
||||||
code: "",
|
|
||||||
path: "",
|
|
||||||
type: 0,
|
|
||||||
icon: "",
|
|
||||||
description: "",
|
|
||||||
sort: 0,
|
|
||||||
is_show: 1,
|
|
||||||
status: 1,
|
|
||||||
};
|
|
||||||
dialogVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleEdit(row) {
|
|
||||||
dialogType.value = "edit";
|
|
||||||
try {
|
|
||||||
const res = await getModuleDetail(row.id);
|
|
||||||
if (res.code === 200 && res.data) {
|
|
||||||
formData.value = { ...res.data };
|
|
||||||
dialogVisible.value = true;
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || "获取模块详情失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取模块详情失败:", error);
|
|
||||||
ElMessage.error("获取模块详情失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
|
||||||
try {
|
|
||||||
await formRef.value.validate();
|
|
||||||
submitLoading.value = true;
|
|
||||||
|
|
||||||
if (dialogType.value === "add") {
|
|
||||||
const res = await addModule(formData.value);
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success("添加成功");
|
|
||||||
dialogVisible.value = false;
|
|
||||||
fetchModules();
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || "添加失败");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const res = await editModule(formData.value.id, formData.value);
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success("编辑成功");
|
|
||||||
dialogVisible.value = false;
|
|
||||||
fetchModules();
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || "编辑失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("提交失败:", error);
|
|
||||||
} finally {
|
|
||||||
submitLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDelete(row) {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm("确定要删除该模块吗?", "提示", {
|
|
||||||
type: "warning",
|
|
||||||
});
|
|
||||||
const res = await deleteModule(row.id);
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success("删除成功");
|
|
||||||
fetchModules();
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || "删除失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error !== "cancel") {
|
|
||||||
console.error("删除失败:", error);
|
|
||||||
ElMessage.error("删除失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleBatchDelete() {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(
|
|
||||||
`确定要删除选中的 ${selectedModules.value.length} 个模块吗?`,
|
|
||||||
"提示",
|
|
||||||
{
|
|
||||||
type: "warning",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const ids = selectedModules.value.map((item) => item.id);
|
|
||||||
const res = await batchDeleteModules(ids);
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success("批量删除成功");
|
|
||||||
fetchModules();
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || "批量删除失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error !== "cancel") {
|
|
||||||
console.error("批量删除失败:", error);
|
|
||||||
ElMessage.error("批量删除失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleShowChange(row) {
|
|
||||||
try {
|
|
||||||
const res = await changeModuleStatus(row.id, row.is_show);
|
|
||||||
if (res.code !== 200) {
|
|
||||||
ElMessage.error(res.msg || "状态修改失败");
|
|
||||||
row.is_show = row.is_show ? 0 : 1;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("状态修改失败:", error);
|
|
||||||
row.is_show = row.is_show ? 0 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchModules();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.container-box {
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--el-bg-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-name {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.module-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tips-section {
|
|
||||||
margin-top: 20px;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 4px 0;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(8, 1fr);
|
|
||||||
gap: 8px;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.icon-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.active {
|
|
||||||
background: var(--el-color-primary-light-9);
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,244 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="container-box">
|
|
||||||
<div class="header-bar">
|
|
||||||
<h2>程序管理</h2>
|
|
||||||
<el-button type="primary" @click="showProgramDialog = true">
|
|
||||||
<el-icon><Plus /></el-icon>
|
|
||||||
添加程序
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider></el-divider>
|
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
|
||||||
<div v-if="loading" class="loading-state">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
<p>正在加载程序数据...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 错误状态 -->
|
|
||||||
<div v-else-if="error" class="error-state">
|
|
||||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
|
||||||
<el-button type="primary" @click="fetchPrograms">重试</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 程序列表 -->
|
|
||||||
<div v-else>
|
|
||||||
<el-table :data="programs" stripe style="width: 100%" v-loading="loading">
|
|
||||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
|
||||||
<el-table-column prop="name" label="程序名称" min-width="160" align="center" />
|
|
||||||
<el-table-column prop="type" label="类型" width="100" align="center" />
|
|
||||||
<el-table-column prop="owner" label="负责人" width="120" align="center" />
|
|
||||||
<el-table-column prop="createdAt" label="创建时间" width="170" align="center" />
|
|
||||||
<el-table-column prop="remark" label="备注" min-width="140" align="center" />
|
|
||||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
|
||||||
<el-button size="small" type="danger" @click="handleDelete(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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 新增/编辑程序弹窗 -->
|
|
||||||
<el-dialog
|
|
||||||
:title="isEditing ? '编辑程序' : '添加程序'"
|
|
||||||
v-model="showProgramDialog"
|
|
||||||
width="420px"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
:model="programForm"
|
|
||||||
:rules="formRules"
|
|
||||||
ref="programFormRef"
|
|
||||||
label-width="90px"
|
|
||||||
>
|
|
||||||
<el-form-item label="程序名称" prop="name">
|
|
||||||
<el-input v-model="programForm.name" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="类型" prop="type">
|
|
||||||
<el-select v-model="programForm.type" placeholder="选择类型">
|
|
||||||
<el-option label="Web" value="Web" />
|
|
||||||
<el-option label="服务" value="Service" />
|
|
||||||
<el-option label="工具" value="Tool" />
|
|
||||||
<el-option label="其他" value="Other" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="负责人" prop="owner">
|
|
||||||
<el-input v-model="programForm.owner" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="备注" prop="remark">
|
|
||||||
<el-input v-model="programForm.remark" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showProgramDialog = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="submitProgramForm"> 保存 </el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, reactive, onMounted } from "vue";
|
|
||||||
import {
|
|
||||||
ElMessage,
|
|
||||||
ElMessageBox,
|
|
||||||
type FormInstance,
|
|
||||||
type FormRules,
|
|
||||||
} from "element-plus";
|
|
||||||
import { Plus } from "@element-plus/icons-vue";
|
|
||||||
|
|
||||||
// 数据与状态
|
|
||||||
const programs = ref<any[]>([]);
|
|
||||||
const loading = ref(false);
|
|
||||||
const error = ref("");
|
|
||||||
const page = ref(1);
|
|
||||||
const pageSize = ref(10);
|
|
||||||
const total = ref(0);
|
|
||||||
|
|
||||||
const showProgramDialog = ref(false);
|
|
||||||
const isEditing = ref(false);
|
|
||||||
const programForm = reactive({
|
|
||||||
id: null,
|
|
||||||
name: "",
|
|
||||||
type: "",
|
|
||||||
owner: "",
|
|
||||||
remark: "",
|
|
||||||
});
|
|
||||||
const programFormRef = ref<FormInstance>();
|
|
||||||
|
|
||||||
const formRules: FormRules = {
|
|
||||||
name: [
|
|
||||||
{ required: true, message: "请输入程序名称", trigger: "blur" },
|
|
||||||
{ min: 2, max: 32, message: "名称长度2-32字符", trigger: "blur" },
|
|
||||||
],
|
|
||||||
type: [{ required: true, message: "请选择类型", trigger: "change" }],
|
|
||||||
owner: [{ required: true, message: "请输入负责人", trigger: "blur" }],
|
|
||||||
};
|
|
||||||
|
|
||||||
async function fetchPrograms() {
|
|
||||||
loading.value = true;
|
|
||||||
error.value = "";
|
|
||||||
try {
|
|
||||||
// TODO: 替换为真实API
|
|
||||||
await new Promise((r) => setTimeout(r, 250));
|
|
||||||
// 假数据
|
|
||||||
const all = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: "门户网站",
|
|
||||||
type: "Web",
|
|
||||||
owner: "张三",
|
|
||||||
createdAt: "2023-11-08 09:22:53",
|
|
||||||
remark: "官网",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "自动备份服务",
|
|
||||||
type: "Service",
|
|
||||||
owner: "李四",
|
|
||||||
createdAt: "2024-01-16 15:40:01",
|
|
||||||
remark: "每日凌晨自动执行",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "运维工具",
|
|
||||||
type: "Tool",
|
|
||||||
owner: "王五",
|
|
||||||
createdAt: "2023-12-02 12:51:29",
|
|
||||||
remark: "",
|
|
||||||
},
|
|
||||||
// ...更多
|
|
||||||
];
|
|
||||||
total.value = all.length;
|
|
||||||
programs.value = all.slice(
|
|
||||||
(page.value - 1) * pageSize.value,
|
|
||||||
page.value * pageSize.value
|
|
||||||
);
|
|
||||||
} catch (err: any) {
|
|
||||||
error.value = err.message || "获取程序列表失败";
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePageChange = (val: number) => {
|
|
||||||
page.value = val;
|
|
||||||
fetchPrograms();
|
|
||||||
};
|
|
||||||
|
|
||||||
function resetProgramForm() {
|
|
||||||
programForm.id = null;
|
|
||||||
programForm.name = "";
|
|
||||||
programForm.type = "";
|
|
||||||
programForm.owner = "";
|
|
||||||
programForm.remark = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEdit(row: any) {
|
|
||||||
isEditing.value = true;
|
|
||||||
programForm.id = row.id;
|
|
||||||
programForm.name = row.name;
|
|
||||||
programForm.type = row.type;
|
|
||||||
programForm.owner = row.owner;
|
|
||||||
programForm.remark = row.remark;
|
|
||||||
showProgramDialog.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDelete(row: any) {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(
|
|
||||||
`确定要删除程序「${row.name}」? 删除后不可恢复。`,
|
|
||||||
"警告",
|
|
||||||
{ type: "warning" }
|
|
||||||
);
|
|
||||||
// TODO: 替换为真实API
|
|
||||||
await new Promise((r) => setTimeout(r, 250));
|
|
||||||
ElMessage.success("删除成功");
|
|
||||||
fetchPrograms();
|
|
||||||
} catch {
|
|
||||||
// 取消删除
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitProgramForm() {
|
|
||||||
await programFormRef.value?.validate();
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
if (isEditing.value) {
|
|
||||||
// TODO: 替换为真实API更新
|
|
||||||
await new Promise((r) => setTimeout(r, 250));
|
|
||||||
ElMessage.success("程序更新成功");
|
|
||||||
} else {
|
|
||||||
// TODO: 替换为真实API新增
|
|
||||||
await new Promise((r) => setTimeout(r, 250));
|
|
||||||
ElMessage.success("程序添加成功");
|
|
||||||
}
|
|
||||||
showProgramDialog.value = false;
|
|
||||||
fetchPrograms();
|
|
||||||
} catch (e: any) {
|
|
||||||
ElMessage.error(e.message || "操作失败,请重试");
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchPrograms();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="sms-edit">
|
|
||||||
<el-form
|
|
||||||
ref="formRef"
|
|
||||||
:model="form"
|
|
||||||
:rules="rules"
|
|
||||||
label-width="120px"
|
|
||||||
class="sms-form"
|
|
||||||
>
|
|
||||||
<el-form-item label="短信网关地址" prop="backendUrl">
|
|
||||||
<el-input
|
|
||||||
v-model="form.backendUrl"
|
|
||||||
placeholder="例如:https://yzsms.yunzer.cn"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="短信API KEY" prop="apiKey">
|
|
||||||
<el-input v-model="form.apiKey" placeholder="请输入 API_KEY" clearable />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="测试收件手机号">
|
|
||||||
<el-input
|
|
||||||
v-model="testPhone"
|
|
||||||
placeholder="国际格式号码,例如 +8613712345678"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" @click="handleSave" :loading="saveLoading">
|
|
||||||
保存配置
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="handleTest" :loading="testLoading">发送测试短信</el-button>
|
|
||||||
<el-button @click="handleReset" :disabled="saveLoading || testLoading">重置</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { reactive, ref, watch } from "vue";
|
|
||||||
import { ElMessage } from "element-plus";
|
|
||||||
import type { FormInstance, FormRules } from "element-plus";
|
|
||||||
import { editSmsInfo, sendTestSms } from "../../../../api/sms";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
config: {
|
|
||||||
backendUrl?: string;
|
|
||||||
apiKey?: string;
|
|
||||||
};
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "saved"): void;
|
|
||||||
(e: "tested"): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const formRef = ref<FormInstance>();
|
|
||||||
const form = reactive({
|
|
||||||
backendUrl: "",
|
|
||||||
apiKey: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const rules: FormRules = {
|
|
||||||
backendUrl: [{ required: true, message: "请输入短信网关地址", trigger: "blur" }],
|
|
||||||
apiKey: [{ required: true, message: "请输入API KEY", trigger: "blur" }],
|
|
||||||
};
|
|
||||||
|
|
||||||
const testPhone = ref("");
|
|
||||||
const saveLoading = ref(false);
|
|
||||||
const testLoading = ref(false);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.config,
|
|
||||||
(v) => {
|
|
||||||
form.backendUrl = v?.backendUrl ?? "";
|
|
||||||
form.apiKey = v?.apiKey ?? "";
|
|
||||||
},
|
|
||||||
{ deep: true, immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
if (!formRef.value) return;
|
|
||||||
try {
|
|
||||||
saveLoading.value = true;
|
|
||||||
await formRef.value.validate();
|
|
||||||
const res = await editSmsInfo(form);
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success(res.msg || "短信配置保存成功");
|
|
||||||
emit("saved");
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || "保存失败,请稍后重试");
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
ElMessage.error(e?.message || "保存失败,请稍后重试");
|
|
||||||
} finally {
|
|
||||||
saveLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTest = async () => {
|
|
||||||
const phone = (testPhone.value || "").trim();
|
|
||||||
if (!phone) {
|
|
||||||
ElMessage.warning("请先输入测试收件手机号");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Android 网关要求国际格式:以 + 开头,后面为数字
|
|
||||||
if (!/^\+\d{6,15}$/.test(phone)) {
|
|
||||||
ElMessage.warning("请使用国际格式号码(以 + 开头,后面为数字)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formRef.value) return;
|
|
||||||
try {
|
|
||||||
testLoading.value = true;
|
|
||||||
await formRef.value.validate();
|
|
||||||
const res = await sendTestSms({ ...form, phone });
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success(res.msg || "短信测试任务入队成功");
|
|
||||||
emit("tested");
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || "短信测试失败,请稍后重试");
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
ElMessage.error(e?.message || "短信测试失败,请稍后重试");
|
|
||||||
} finally {
|
|
||||||
testLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
form.backendUrl = "";
|
|
||||||
form.apiKey = "";
|
|
||||||
testPhone.value = "";
|
|
||||||
formRef.value?.clearValidate();
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.sms-edit {
|
|
||||||
max-width: 700px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sms-form {
|
|
||||||
max-width: 700px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
|
|
||||||
<template>
|
|
||||||
<div class="container-box">
|
|
||||||
<div class="header-bar">
|
|
||||||
<h2>短信配置</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider />
|
|
||||||
|
|
||||||
<div class="content-box">
|
|
||||||
<Edit :config="smsForm" @saved="loadSmsConfig" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { reactive, onMounted } from "vue";
|
|
||||||
import Edit from "./components/edit.vue";
|
|
||||||
import { getSmsInfo } from "../../../api/sms";
|
|
||||||
|
|
||||||
const smsForm = reactive({
|
|
||||||
backendUrl: "",
|
|
||||||
apiKey: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadSmsConfig = async () => {
|
|
||||||
const res = await getSmsInfo();
|
|
||||||
if (res.code === 200 && Array.isArray(res.data) && res.data.length > 0) {
|
|
||||||
const item = res.data[0];
|
|
||||||
smsForm.backendUrl = item.backend_url || item.backendUrl || "";
|
|
||||||
smsForm.apiKey = item.api_key || item.apiKey || "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadSmsConfig();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.container-box {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-box {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,189 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="container-box">
|
|
||||||
<div class="header-bar">
|
|
||||||
<h2>短信任务列表</h2>
|
|
||||||
<div>
|
|
||||||
<el-button type="primary" @click="handleSearch">
|
|
||||||
搜索
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="fetchTasks" :loading="loading">
|
|
||||||
<el-icon><Refresh /></el-icon>
|
|
||||||
刷新
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider />
|
|
||||||
|
|
||||||
<div class="search-bar">
|
|
||||||
<el-form :inline="true" :model="searchForm">
|
|
||||||
<el-form-item label="手机号">
|
|
||||||
<el-input
|
|
||||||
v-model="searchForm.phone"
|
|
||||||
placeholder="请输入手机号关键词"
|
|
||||||
style="width: 220px"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="状态">
|
|
||||||
<el-select v-model="searchForm.status" placeholder="全部" style="width: 160px">
|
|
||||||
<el-option label="全部" value="" />
|
|
||||||
<el-option label="待发送" :value="0" />
|
|
||||||
<el-option label="处理中" :value="1" />
|
|
||||||
<el-option label="失败" :value="2" />
|
|
||||||
<el-option label="已上报" :value="3" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item>
|
|
||||||
<el-button @click="resetSearch">重置</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table v-loading="loading" :data="pagedList" style="width: 100%" border>
|
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
|
||||||
<el-table-column prop="phone" label="手机号" min-width="150" />
|
|
||||||
<el-table-column
|
|
||||||
prop="content"
|
|
||||||
label="短信内容"
|
|
||||||
min-width="260"
|
|
||||||
show-overflow-tooltip
|
|
||||||
/>
|
|
||||||
<el-table-column prop="code" label="验证码" width="120" />
|
|
||||||
<el-table-column prop="status" label="状态" width="130">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="getStatusTagType(row.status)">
|
|
||||||
{{ getStatusText(row.status) }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="create_time" label="创建时间" width="180" />
|
|
||||||
<el-table-column prop="update_time" label="更新时间" width="180" />
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<div class="pagination" v-if="total > 0">
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="pagination.current"
|
|
||||||
v-model:page-size="pagination.size"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
:total="total"
|
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
|
||||||
@size-change="applyPagination"
|
|
||||||
@current-change="applyPagination"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { reactive, ref, onMounted } from "vue";
|
|
||||||
import { Refresh } from "@element-plus/icons-vue";
|
|
||||||
import { ElMessage } from "element-plus";
|
|
||||||
import { getSmsTaskList } from "../../../api/sms";
|
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
const searchForm = reactive({
|
|
||||||
phone: "",
|
|
||||||
status: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const tasks = ref<any[]>([]);
|
|
||||||
const pagedList = ref<any[]>([]);
|
|
||||||
|
|
||||||
const pagination = reactive({
|
|
||||||
current: 1,
|
|
||||||
size: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const total = ref(0);
|
|
||||||
|
|
||||||
const getStatusTagType = (status: any) => {
|
|
||||||
const map: Record<string | number, string> = {
|
|
||||||
0: "info",
|
|
||||||
1: "warning",
|
|
||||||
2: "danger",
|
|
||||||
3: "success",
|
|
||||||
};
|
|
||||||
return map[status] || "info";
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = (status: any) => {
|
|
||||||
const map: Record<string | number, string> = {
|
|
||||||
0: "待发送",
|
|
||||||
1: "发送中",
|
|
||||||
2: "发送失败",
|
|
||||||
3: "发送成功",
|
|
||||||
};
|
|
||||||
return map[status] || String(status ?? "");
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyPagination = () => {
|
|
||||||
total.value = tasks.value.length;
|
|
||||||
const start = (pagination.current - 1) * pagination.size;
|
|
||||||
const end = start + pagination.size;
|
|
||||||
pagedList.value = tasks.value.slice(start, end);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchTasks = async () => {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const res = await getSmsTaskList({
|
|
||||||
status: searchForm.status,
|
|
||||||
phone: searchForm.phone,
|
|
||||||
});
|
|
||||||
if (res.code === 200) {
|
|
||||||
tasks.value = res.list || [];
|
|
||||||
pagination.current = 1;
|
|
||||||
applyPagination();
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || "获取短信任务列表失败");
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || "获取短信任务列表失败,请稍后重试");
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
|
||||||
pagination.current = 1;
|
|
||||||
fetchTasks();
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetSearch = () => {
|
|
||||||
searchForm.phone = "";
|
|
||||||
searchForm.status = "";
|
|
||||||
pagination.current = 1;
|
|
||||||
fetchTasks();
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchTasks();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.container-box {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-bar {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user