更新短信功能

This commit is contained in:
李志强 2026-04-01 15:14:37 +08:00
parent b870115444
commit e927026495
22 changed files with 581 additions and 307 deletions

View File

@ -12,13 +12,15 @@ export function getUserCate() {
}
/**
* 获取所有文件
* 获取所有文件支持分页分类关键词与后端 query 一致
* @param {Object} [params] page, pageSize, cate, keyword
* @returns {Promise}
*/
export function getAllFiles() {
export function getAllFiles(params = {}) {
return request({
url: "/platform/allfiles",
method: "get",
method: "get",
params,
});
}
@ -85,13 +87,13 @@ export function getCateFiles(id, page = 1, pageSize = 24, keyword = "") {
}
/**
* 根据ID获取文件
* @param {number|string} id 文件ID
* 根据文件 ID 获取单条文件信息非分类列表
* @param {number|string} id 文件主键 ID
* @returns {Promise}
*/
export function getFileById(id) {
return request({
url: `/platform/catefiles`,
url: `/platform/file/${id}`,
method: "get",
});
}
@ -100,12 +102,17 @@ export function getFileById(id) {
* 上传文件
* @param {FormData} formData 文件数据
* @param {Object} options 额外选项
* @param {string} [options.cate]
* @param {string|number} [options.cate] 文件分组0 为未分类
* @param {string|number} [options.tuid] 租户用户 yz_tenant_user.id租户侧上传时传平台管理员不传
* @returns {Promise}
*/
export function uploadFile(formData, options = {}) {
if (options.cate) {
formData.append('cate', options.cate);
// 0 表示「未分类」,不能用 truthy 判断
if (options.cate !== undefined && options.cate !== null && options.cate !== "") {
formData.append("cate", String(options.cate));
}
if (options.tuid !== undefined && options.tuid !== null && options.tuid !== "") {
formData.append("tuid", String(options.tuid));
}
return request({
@ -151,7 +158,7 @@ export function deleteFile(id) {
*/
export function deleteFilePermanently(id) {
return request({
url: `/platform/deleteFilePermanently/${id}`,
url: `/platform/deletefilepermanently/${id}`,
method: "delete",
});
}
@ -177,7 +184,7 @@ export function moveFile(id, cate) {
*/
export function batchDeleteFiles(ids) {
return request({
url: "/platform/batchDeleteFiles",
url: "/platform/batchdeletefiles",
method: "post",
data: { ids },
});

View File

@ -9,6 +9,14 @@ export function login(data) {
});
}
/** 当前登录用户信息(含角色名称),需携带 token */
export function getCurrentUser() {
return request({
url: `/platform/currentUser`,
method: "get",
});
}
// 发送登录验证码(手机号)
export function sendLoginCode(data) {
return request({

View File

@ -1,6 +1,6 @@
import request from "@/utils/request";
// 获取租户用户列表
/** 获取租户用户列表params 可含 tid、uid、keyword模糊匹配姓名/手机/邮箱/账号) */
export function getTenantUserList(params) {
return request({
url: "/platform/tenantUser/list",
@ -9,7 +9,7 @@ export function getTenantUserList(params) {
});
}
// 创建租户用户绑定关系
/** 创建租户用户绑定(后端写入 yz_tenant_user */
export function createTenantUser(data) {
return request({
url: "/platform/tenantUser/create",

View File

@ -43,6 +43,7 @@
<span class="el-dropdown-link" style="cursor: pointer;">
<img :src="getImageUrl('user')" class="user" />
<span class="user-name">{{ displayName }}</span>
<el-tag v-if="roleLabel" size="small" effect="plain" class="user-role-tag">{{ roleLabel }}</el-tag>
</span>
<template #dropdown>
<el-dropdown-menu>
@ -73,7 +74,7 @@ import { ref, computed, onMounted, onUnmounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useAllDataStore, useMenuStore, useTabsStore } from "@/stores";
import { useAuthStore } from "@/stores/auth";
import { logout } from "@/api/login";
import { logout, getCurrentUser } from "@/api/login";
import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
@ -130,7 +131,18 @@ async function refreshCache() {
}
}
onMounted(loadMenu);
onMounted(async () => {
await loadMenu();
if (!authStore.token) return;
try {
const res = await getCurrentUser();
if (res && res.code === 200 && res.data) {
authStore.updateUserInfo({ ...authStore.user, ...res.data });
}
} catch (e) {
console.error("getCurrentUser failed", e);
}
});
//
const breadcrumbs = computed(() => {
@ -179,6 +191,12 @@ const displayName = computed(() => {
return user.account || '';
});
/** 角色展示名(来自 yz_admin_role.name */
const roleLabel = computed(() => {
const n = authStore.user?.role_name;
return typeof n === "string" && n.trim() ? n.trim() : "";
});
const handleCollapse = () => {
store.state.isCollapse = !store.state.isCollapse;
};
@ -424,6 +442,18 @@ onUnmounted(() => {
color: #ffffff;
}
}
.user-role-tag {
flex-shrink: 0;
margin-left: 4px;
font-weight: 500;
}
html:not(.dark) & .user-role-tag {
color: #ffffff;
border-color: rgba(255, 255, 255, 0.85);
background: rgba(255, 255, 255, 0.12);
}
}
}

View File

@ -15,6 +15,12 @@ const staticMainChildren = [
component: () => import("@/views/user/userProfile.vue"),
meta: { requiresAuth: true, title: "用户中心" }
},
{
path: "/system/email",
name: "SystemEmail",
component: () => import("@/views/system/email/index.vue"),
meta: { requiresAuth: true, title: "邮箱管理" }
},
// 兼容拼写错误的路径重定向
{
path: "/apps/erp/dashborad",

View File

@ -1,13 +1,39 @@
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
// 用户信息类型
// 平台登录缓存仅保留以下字段(不含 tid、group_id
const defaultUser = {
id:'',
id: '',
account: '',
name: '',
rid: '',
avatar: ''
avatar: '',
role_name: ''
}
/**
* 规范化写入 localStorage 的用户信息去掉 tidgroup_id 等废弃字段
* 若仅有历史 group_id则迁移到 rid
*/
function normalizePlatformUser(raw) {
if (!raw || typeof raw !== 'object') {
return { ...defaultUser }
}
let rid = raw.rid
if (rid === undefined || rid === null || rid === '') {
const legacy = raw.group_id
if (legacy !== undefined && legacy !== null && legacy !== '') {
rid = legacy
}
}
return {
id: parseInt(raw.id, 10) || null,
account: raw.account || '',
name: raw.name || '',
rid: rid !== undefined && rid !== null && rid !== '' ? rid : '',
avatar: raw.avatar || '',
role_name: raw.role_name || ''
}
}
export const useAuthStore = defineStore('auth', () => {
@ -20,8 +46,10 @@ export const useAuthStore = defineStore('auth', () => {
const cachedUser = localStorage.getItem('userInfo')
if (cachedUser) {
try {
const userInfo = JSON.parse(cachedUser)
Object.assign(user, userInfo)
const parsed = JSON.parse(cachedUser)
const normalized = normalizePlatformUser(parsed)
Object.assign(user, normalized)
localStorage.setItem('userInfo', JSON.stringify(normalized))
} catch (e) {
console.error('Failed to parse user info from cache:', e)
}
@ -34,16 +62,8 @@ export const useAuthStore = defineStore('auth', () => {
// 保存登录信息token 和用户信息)
function setLoginInfo(loginData) {
const userInfo = loginData.user || loginData
const normalizedUser = normalizePlatformUser(userInfo)
const normalizedUser = {
id: parseInt(userInfo.id) || null,
account: userInfo.account || '',
name: userInfo.name || '',
rid: userInfo.rid || userInfo.group_id || '',
avatar: userInfo.avatar || ''
}
// 使用后端返回的真实 JWT token
const accessToken = loginData.token || ''
token.value = accessToken
@ -65,7 +85,7 @@ export const useAuthStore = defineStore('auth', () => {
function clearToken() {
token.value = ''
isLoggedIn.value = false
Object.assign(user, defaultUser)
Object.assign(user, { ...defaultUser })
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
}
@ -80,14 +100,15 @@ export const useAuthStore = defineStore('auth', () => {
} else {
token.value = ''
isLoggedIn.value = false
Object.assign(user, defaultUser)
Object.assign(user, { ...defaultUser })
}
}
// 更新用户信息
function updateUserInfo(userInfo) {
Object.assign(user, userInfo)
localStorage.setItem('userInfo', JSON.stringify(userInfo))
// 更新用户信息(合并后仍只持久化平台字段)
function updateUserInfo(partial) {
const merged = normalizePlatformUser({ ...user, ...partial })
Object.assign(user, merged)
localStorage.setItem('userInfo', JSON.stringify(merged))
}
return {
@ -101,4 +122,3 @@ export const useAuthStore = defineStore('auth', () => {
updateUserInfo
}
})

View File

@ -21,7 +21,7 @@ export const useMenuStore = defineStore('menu', () => {
try {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
const loginType = userInfo.type || 'user';
const roleId = userInfo.group_id || 0;
const roleId = userInfo.rid || 0;
return `menu_cache_${loginType}_${roleId}`;
} catch (e) {
return 'menu_cache_default';
@ -109,7 +109,7 @@ export const useMenuStore = defineStore('menu', () => {
try {
const userInfo = getUserInfo();
const loginType = userInfo.type || 'user';
const roleId = userInfo.group_id || 0;
const roleId = userInfo.rid || 0;
let res;

View File

@ -1,7 +1,7 @@
import axios from 'axios';
// 获取API基础URL,添加调试信息
const apiBaseURL = import.meta.env.VITE_API_BASE_URL;
// 获取API基础URL;开发环境可在 .env.development 留空,配合 Vite 代理访问 /platform
const apiBaseURL = import.meta.env.VITE_API_BASE_URL ?? "";
// 创建axios实例
const service = axios.create({

View File

@ -37,8 +37,8 @@
{{ user.email || "未设置" }}
</el-descriptions-item>
<el-descriptions-item label="角色">
<el-tag :type="getRoleTagType(user.group_id)" size="small">
{{ getRoleName(user.group_id) }}
<el-tag :type="getRoleTagType(user.rid)" size="small">
{{ getRoleName(user.rid) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="最后登录IP">
@ -75,7 +75,7 @@ interface User {
qq: string;
email: string;
sex: number;
group_id: number;
rid: number;
status: number;
last_login_ip: string;
login_count: number;
@ -128,19 +128,19 @@ const fetchRoles = async () => {
};
// tag
function getRoleTagType(group_id: number): string {
function getRoleTagType(rid: number): string {
const typeMap: Record<number, string> = {
1: "primary",
2: "success",
3: "warning",
4: "danger",
};
return typeMap[group_id] || "primary";
return typeMap[rid] || "primary";
}
//
function getRoleName(group_id: number): string {
const role = roles.value.find((r) => r.id === group_id);
function getRoleName(rid: number): string {
const role = roles.value.find((r) => r.id === rid);
return role?.name || "未知";
}

View File

@ -141,7 +141,7 @@ interface User {
phone: string;
qq: string;
sex: number;
group_id: number;
rid: number;
status: number;
last_login_ip: string;
email: number;

View File

@ -26,7 +26,7 @@
/>
</el-tab-pane>
<el-tab-pane label="登录验证" v-if="groupId === 1" name="loginVerification">
<el-tab-pane label="登录验证" v-if="roleId === 1" name="loginVerification">
<loginVerificationSettings
ref="loginVerificationSettingsRef"
v-if="activeTab === 'loginVerification'"
@ -49,7 +49,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ref, computed } from "vue";
import normalSettings from "./components/normalSettings.vue";
import seoSettings from "./components/seoSettings.vue";
import contactSettings from "./components/contactSettings.vue";
@ -59,7 +59,7 @@ import loginVerificationSettings from "./components/loginVerification.vue";
import { useAuthStore } from '@/stores/auth';
const authStore = useAuthStore();
const groupId = (authStore.userInfo as any)?.group_id;
const roleId = computed(() => Number(authStore.user?.rid) || 0);
const activeTab = ref("basic");
</script>

View File

@ -0,0 +1,244 @@
<template>
<div class="tenant-users-tab">
<div class="section-header">
<div class="section-title">用户列表</div>
<el-button type="primary" size="small" :disabled="!tid" @click="handleAddUser">
<el-icon><Plus /></el-icon>
添加用户
</el-button>
</div>
<el-form :inline="true" class="user-search-form" @submit.prevent>
<el-form-item label="关键词">
<el-input
v-model="userSearchKeyword"
clearable
placeholder="姓名 / 手机 / 邮箱 / 账号"
style="width: 260px"
:disabled="!tid"
@keyup.enter="handleSearchUsers"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :disabled="!tid" @click="handleSearchUsers">查询</el-button>
<el-button :disabled="!tid" @click="resetUserSearch">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="tenantUsers" v-loading="usersLoading" border style="width: 100%">
<el-table-column prop="account" label="用户名" min-width="140" align="center" />
<el-table-column prop="name" label="姓名" min-width="120" align="center" />
<el-table-column prop="phone" label="手机号" min-width="140" align="center" />
<el-table-column prop="email" label="邮箱" min-width="180" align="center" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="Number(row.status) === 1 ? 'success' : 'danger'">
{{ Number(row.status) === 1 ? "启用" : "禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="140" align="center" fixed="right">
<template #default="{ row }">
<el-button text type="primary" @click="openPasswordDialog(row)">
修改密码
</el-button>
</template>
</el-table-column>
</el-table>
<AddUser ref="addUserRef" @success="refreshTenantUsers" />
<el-dialog v-model="passwordDialogVisible" title="修改密码" width="420px" destroy-on-close>
<el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules" label-width="90px">
<el-form-item label="新密码" prop="password">
<el-input v-model="passwordForm.password" type="password" show-password placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认密码" prop="password2">
<el-input v-model="passwordForm.password2" type="password" show-password placeholder="请再次输入新密码" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="passwordDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="passwordSubmitting" @click="submitPassword">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from "vue";
import { ElMessage } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
import { getTenantUserList, editTenantUser } from "@/api/tenantUser";
import AddUser from "./adduser.vue";
const props = defineProps<{
/** 当前租户 ID为空时不请求 */
tid: number | null;
}>();
const tenantUsers = ref<any[]>([]);
const usersLoading = ref(false);
const userSearchKeyword = ref("");
const addUserRef = ref<{ open: (tenantId: number) => void } | null>(null);
const passwordDialogVisible = ref(false);
const passwordSubmitting = ref(false);
const currentTenantUserId = ref<number | null>(null);
const passwordFormRef = ref();
const passwordForm = reactive({
password: "",
password2: "",
});
const passwordRules = {
password: [
{ required: true, message: "请输入新密码", trigger: "blur" },
{ min: 6, message: "密码至少 6 位", trigger: "blur" },
],
password2: [
{ required: true, message: "请再次输入新密码", trigger: "blur" },
{
validator: (_rule: any, value: string, callback: any) => {
if (!value) return callback(new Error("请再次输入新密码"));
if (value !== passwordForm.password) return callback(new Error("两次密码不一致"));
callback();
},
trigger: "blur",
},
],
};
const buildTenantUserQuery = (tid: number) => {
const params: Record<string, string | number> = { tid };
const kw = userSearchKeyword.value.trim();
if (kw) params.keyword = kw;
return params;
};
const refreshTenantUsers = async () => {
const id = props.tid;
if (id == null) return;
usersLoading.value = true;
try {
const usersRes = await getTenantUserList(buildTenantUserQuery(id));
if (usersRes?.code === 200) {
tenantUsers.value = usersRes?.data?.list || usersRes?.data || [];
} else {
tenantUsers.value = [];
}
} catch {
tenantUsers.value = [];
} finally {
usersLoading.value = false;
}
};
const loadUsersForTid = async (tid: number) => {
usersLoading.value = true;
try {
const usersRes = await getTenantUserList({ tid });
if (usersRes?.code === 200) {
tenantUsers.value = usersRes?.data?.list || usersRes?.data || [];
} else {
tenantUsers.value = [];
}
} catch {
tenantUsers.value = [];
} finally {
usersLoading.value = false;
}
};
watch(
() => props.tid,
(id) => {
userSearchKeyword.value = "";
if (id == null) {
tenantUsers.value = [];
return;
}
loadUsersForTid(id);
},
{ immediate: true }
);
const handleSearchUsers = () => {
refreshTenantUsers();
};
const resetUserSearch = () => {
userSearchKeyword.value = "";
refreshTenantUsers();
};
const handleAddUser = () => {
if (props.tid != null) {
addUserRef.value?.open(props.tid);
}
};
const openPasswordDialog = (row: any) => {
currentTenantUserId.value = Number(row?.id || 0) || null;
passwordForm.password = "";
passwordForm.password2 = "";
passwordDialogVisible.value = true;
};
const submitPassword = async () => {
if (!passwordFormRef.value || !currentTenantUserId.value) return;
try {
await passwordFormRef.value.validate();
} catch {
return;
}
passwordSubmitting.value = true;
try {
const res = await editTenantUser(currentTenantUserId.value, { password: passwordForm.password });
if (res.code === 200) {
ElMessage.success("密码修改成功");
passwordDialogVisible.value = false;
} else {
ElMessage.error(res.msg || "密码修改失败");
}
} finally {
passwordSubmitting.value = false;
}
};
defineExpose({
refreshTenantUsers,
});
</script>
<style scoped>
.tenant-users-tab {
min-width: 0;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.user-search-form {
margin-bottom: 8px;
padding: 8px 12px;
background: var(--el-fill-color-light);
border-radius: 4px;
}
.user-search-form :deep(.el-form-item) {
margin-bottom: 8px;
margin-top: 0;
}
</style>

View File

@ -37,6 +37,10 @@
</template>
<script setup lang="ts">
/**
* 租户详情内添加用户提交到 /platform/tenantUser/create
* 后端写入 yz_tenant_user租户-用户绑定含冗余账号/密码等字段
*/
import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { createTenantUser } from '@/api/tenantUser';
@ -60,7 +64,7 @@ const formData = reactive({
const rules = {
account: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur', min: 6 }],
password: [{ required: true, message: '请输入密码', trigger: 'blur', min: 5 }],
password2: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{
@ -103,7 +107,12 @@ const open = (tenantId: number) => {
const submitForm = async () => {
if (!formRef.value) return;
await formRef.value.validate();
try {
await formRef.value.validate();
} catch {
// validate reject Uncaught (in promise)
return;
}
submitting.value = true;

View File

@ -1,221 +1,160 @@
<template>
<el-drawer
v-model="visible"
title="租户详细信息"
size="1000px"
@closed="handleClosed"
>
<div v-loading="loading" class="detail-container">
<el-descriptions
:column="2"
:label-width="110"
border
class="tenant-detail-descriptions"
<el-drawer
v-model="visible"
title="租户详细信息"
size="1200px"
@closed="handleClosed"
>
<div v-loading="loading" class="detail-container">
<el-descriptions
:column="2"
:label-width="110"
border
class="tenant-detail-descriptions"
>
<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="租户地址" :span="2">
{{ detailData.address || '-' }}
</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.create_time || '-' }}
</el-descriptions-item>
</el-descriptions>
<el-divider />
<div class="tenant-detail-body">
<el-tabs
v-model="activeTab"
tab-position="left"
class="tenant-detail-tabs"
>
<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="租户地址" :span="2">
{{ detailData.address || '-' }}
</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.create_time || '-' }}
</el-descriptions-item>
</el-descriptions>
<div class="users-section">
<div class="section-title">租户用户列表</div>
<el-table :data="tenantUsers" v-loading="usersLoading" border style="width: 100%">
<el-table-column prop="account" label="用户名" min-width="140" align="center" />
<el-table-column prop="name" label="姓名" min-width="120" align="center" />
<el-table-column prop="phone" label="手机号" min-width="140" align="center" />
<el-table-column prop="email" label="邮箱" min-width="180" align="center" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="Number(row.status) === 1 ? 'success' : 'danger'">
{{ Number(row.status) === 1 ? "启用" : "禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="140" align="center" fixed="right">
<template #default="{ row }">
<el-button text type="primary" @click="openPasswordDialog(row)">
修改密码
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="footer-actions">
<el-button @click="visible = false">关闭</el-button>
</div>
<el-tab-pane label="租户用户" name="users">
<div class="tab-pane-inner">
<TenantUsersTab :tid="detailTenantId" />
</div>
</el-tab-pane>
<!-- 后续功能在此继续增加 <el-tab-pane label="..." name="...">...</el-tab-pane> -->
</el-tabs>
</div>
</el-drawer>
<el-dialog v-model="passwordDialogVisible" title="修改密码" width="420px" destroy-on-close>
<el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules" label-width="90px">
<el-form-item label="新密码" prop="password">
<el-input v-model="passwordForm.password" type="password" show-password placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认密码" prop="password2">
<el-input v-model="passwordForm.password2" type="password" show-password placeholder="请再次输入新密码" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="passwordDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="passwordSubmitting" @click="submitPassword">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { ElMessage } from "element-plus";
import { getTenantDetail } from '@/api/tenant';
import { getTenantUserList, editTenantUser } from "@/api/tenantUser";
const visible = ref(false);
const loading = ref(false);
const usersLoading = ref(false);
const detailData = ref<any>({});
const tenantUsers = ref<any[]>([]);
<!-- <div class="footer-actions">
<el-button @click="visible = false">关闭</el-button>
</div> -->
</div>
</el-drawer>
</template>
const passwordDialogVisible = ref(false);
const passwordSubmitting = ref(false);
const currentTenantUserId = ref<number | null>(null);
const passwordFormRef = ref();
const passwordForm = reactive({
password: "",
password2: "",
});
<script setup lang="ts">
import { ref } from "vue";
import { getTenantDetail } from "@/api/tenant";
import TenantUsersTab from "./TenantUsersTab.vue";
const passwordRules = {
password: [
{ required: true, message: "请输入新密码", trigger: "blur" },
{ min: 6, message: "密码至少 6 位", trigger: "blur" },
],
password2: [
{ required: true, message: "请再次输入新密码", trigger: "blur" },
{
validator: (_rule: any, value: string, callback: any) => {
if (!value) return callback(new Error("请再次输入新密码"));
if (value !== passwordForm.password) return callback(new Error("两次密码不一致"));
callback();
},
trigger: "blur",
},
],
};
const visible = ref(false);
const loading = ref(false);
const detailData = ref<any>({});
const detailTenantId = ref<number | null>(null);
const activeTab = ref("users");
const openPasswordDialog = (row: any) => {
currentTenantUserId.value = Number(row?.id || 0) || null;
passwordForm.password = "";
passwordForm.password2 = "";
passwordDialogVisible.value = true;
};
const submitPassword = async () => {
if (!passwordFormRef.value || !currentTenantUserId.value) return;
await passwordFormRef.value.validate();
passwordSubmitting.value = true;
try {
const res = await editTenantUser(currentTenantUserId.value, { password: passwordForm.password });
if (res.code === 200) {
ElMessage.success("密码修改成功");
passwordDialogVisible.value = false;
} else {
ElMessage.error(res.msg || "密码修改失败");
}
} finally {
passwordSubmitting.value = false;
const open = async (id: number) => {
detailTenantId.value = id;
activeTab.value = "users";
visible.value = true;
loading.value = true;
try {
const detailRes = await getTenantDetail(id);
if (detailRes.code === 200) {
detailData.value = detailRes.data;
}
};
//
const open = async (id: number) => {
visible.value = true;
loading.value = true;
usersLoading.value = true;
try {
const [detailRes, usersRes] = await Promise.all([
getTenantDetail(id),
getTenantUserList({ tid: id }),
]);
} catch (error) {
console.error("获取详情失败", error);
} finally {
loading.value = false;
}
};
if (detailRes.code === 200) {
detailData.value = detailRes.data;
}
if (usersRes?.code === 200) {
tenantUsers.value = usersRes?.data?.list || usersRes?.data || [];
} else {
tenantUsers.value = [];
}
} catch (error) {
console.error('获取详情失败', error);
tenantUsers.value = [];
} finally {
loading.value = false;
usersLoading.value = false;
}
};
const handleClosed = () => {
detailData.value = {};
tenantUsers.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;
}
.users-section {
margin-top: 20px;
}
.section-title {
margin-bottom: 12px;
font-size: 15px;
font-weight: 600;
color: #303133;
}
:deep(.el-descriptions__label) {
width: 120px;
background-color: #f5f7fa;
}
const handleClosed = () => {
detailData.value = {};
detailTenantId.value = null;
activeTab.value = "users";
};
.tenant-detail-descriptions :deep(.el-descriptions__table) {
width: 100%;
table-layout: fixed;
}
</style>
defineExpose({ open });
</script>
<style scoped>
.detail-container {
padding: 10px 20px;
}
.detail-value {
font-weight: bold;
color: #333;
}
.footer-actions {
margin-top: 24px;
display: flex;
justify-content: flex-end;
}
.tenant-detail-body {
margin-top: 20px;
min-height: 0;
}
.tenant-detail-tabs {
width: 100%;
}
.tenant-detail-tabs :deep(.el-tabs__header) {
margin-right: 0;
}
.tenant-detail-tabs :deep(.el-tabs__nav-wrap.is-left) {
min-width: 112px;
}
.tenant-detail-tabs :deep(.el-tabs__item) {
justify-content: flex-start;
padding: 0 16px;
height: 44px;
line-height: 44px;
}
.tenant-detail-tabs :deep(.el-tabs__content) {
padding: 0 0 0 16px;
min-height: 320px;
}
.tab-pane-inner {
min-width: 0;
}
:deep(.el-descriptions__label) {
width: 120px;
background-color: #f5f7fa;
}
.tenant-detail-descriptions :deep(.el-descriptions__table) {
width: 100%;
table-layout: fixed;
}
</style>

View File

@ -111,14 +111,9 @@
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<el-table-column label="操作" width="160" align="center" fixed="right">
<template #default="scope">
<div class="action-icons">
<el-tooltip content="增加用户" placement="top">
<el-button text size="small" @click="handleAddUser(scope.row)">
<el-icon><UserFilled /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="编辑" placement="top">
<el-button text size="small" @click="editRef.open(scope.row.id)">
<el-icon><Edit /></el-icon>
@ -142,7 +137,6 @@
<EditModal ref="editRef" @success="refresh" />
<DetailDrawer ref="detailRef" />
<Qualification ref="qualificationRef" />
<AddUser ref="addUserRef" />
<!-- 分页 -->
<div class="pagination-bar">
@ -165,8 +159,7 @@ import { ElMessage, ElMessageBox } from "element-plus";
import EditModal from "./components/edit.vue";
import DetailDrawer from "./components/detail.vue";
import Qualification from "./components/qualification.vue";
import AddUser from "./components/adduser.vue";
import { UserFilled, Edit, Delete } from "@element-plus/icons-vue";
import { Edit, Delete } from "@element-plus/icons-vue";
const total = ref(0);
const page = ref(1);
@ -177,7 +170,6 @@ const tenants = ref([]);
const editRef = ref();
const detailRef = ref();
const addUserRef = ref();
const qualificationRef = ref();
//
@ -201,11 +193,6 @@ 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);

View File

@ -37,8 +37,8 @@
{{ user.email || "未设置" }}
</el-descriptions-item>
<el-descriptions-item label="角色">
<el-tag :type="getRoleTagType(user.group_id)" size="small">
{{ getRoleName(user.group_id) }}
<el-tag :type="getRoleTagType(user.rid)" size="small">
{{ getRoleName(user.rid) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="最后登录IP">
@ -75,7 +75,7 @@ interface User {
qq: string;
email: string;
sex: number;
group_id: number;
rid: number;
status: number;
last_login_ip: string;
login_count: number;
@ -128,19 +128,19 @@ const fetchRoles = async () => {
};
// tag
function getRoleTagType(group_id: number): string {
function getRoleTagType(rid: number): string {
const typeMap: Record<number, string> = {
1: "primary",
2: "success",
3: "warning",
4: "danger",
};
return typeMap[group_id] || "primary";
return typeMap[rid] || "primary";
}
//
function getRoleName(group_id: number): string {
const role = roles.value.find((r) => r.id === group_id);
function getRoleName(rid: number): string {
const role = roles.value.find((r) => r.id === rid);
return role?.name || "未知";
}

View File

@ -52,8 +52,8 @@
</el-form-item>
<!-- 角色 -->
<el-form-item label="角色" prop="group_id">
<el-select v-model="form.group_id" placeholder="请选择角色" style="width: 100%">
<el-form-item label="角色" prop="rid">
<el-select v-model="form.rid" placeholder="请选择角色" style="width: 100%">
<el-option
v-for="role in roleOptions"
:key="role.id"
@ -125,7 +125,7 @@ const form = ref<any>({
name: "",
phone: "",
qq: "",
group_id: undefined,
rid: undefined,
sex: 1,
password: "",
confirmPassword: "",
@ -194,7 +194,7 @@ const rules = {
{ min: 3, max: 20, message: "账号长度在 3 到 20 个字符", trigger: "blur" },
],
name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
group_id: [{ required: true, message: "请选择角色", trigger: "change" }],
rid: [{ required: true, message: "请选择角色", trigger: "change" }],
password: [{ validator: validatePassword, trigger: "blur" }],
confirmPassword: [{ validator: validateConfirmPassword, trigger: "blur" }],
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }],
@ -251,7 +251,7 @@ const loadUserData = async (user: any) => {
name: data.name,
phone: data.phone,
qq: data.qq,
group_id: data.group_id,
rid: data.rid,
sex: sexValue,
password: "",
confirmPassword: "",
@ -323,7 +323,7 @@ const handleSubmit = async () => {
name: form.value.name,
phone: form.value.phone,
qq: form.value.qq,
group_id: form.value.group_id,
rid: form.value.rid,
sex: form.value.sex,
email: form.value.email,
status: form.value.status,
@ -345,7 +345,7 @@ const handleSubmit = async () => {
name: form.value.name,
phone: form.value.phone,
qq: form.value.qq,
group_id: form.value.group_id,
rid: form.value.rid,
sex: form.value.sex,
email: form.value.email,
status: form.value.status,
@ -380,7 +380,7 @@ defineExpose({
name: "",
phone: "",
qq: "",
group_id: undefined,
rid: undefined,
sex: 1,
password: "",
confirmPassword: "",
@ -417,7 +417,7 @@ defineExpose({
name: "",
phone: "",
qq: "",
group_id: undefined,
rid: undefined,
sex: 1,
password: "",
confirmPassword: "",

View File

@ -28,10 +28,10 @@
<span class="name-link" @click="handlePreview(scope.row)">{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column prop="group_id" label="角色" align="center">
<el-table-column prop="rid" label="角色" align="center">
<template #default="scope">
<el-tag :type="getRoleTagType(scope.row.group_id)" size="small">
{{ getRoleTagText(roles, scope.row.group_id) }}
<el-tag :type="getRoleTagType(scope.row.rid)" size="small">
{{ getRoleTagText(roles, scope.row.rid) }}
</el-tag>
</template>
</el-table-column>
@ -98,7 +98,7 @@ interface User {
phone: string;
qq: string;
sex: number;
group_id: number;
rid: number;
status: number;
last_login_ip: string;
login_count: number;
@ -153,22 +153,22 @@ const fetchRoles = async () => {
};
// tag
function getRoleTagType(group_id: number): string {
function getRoleTagType(rid: number): string {
const typeMap: Record<number, string> = {
1: "primary",
2: "success",
3: "warning",
4: "danger",
};
return typeMap[group_id] || "primary";
return typeMap[rid] || "primary";
}
// tag
function getRoleTagText(roles: Role[] | undefined, group_id: number): string {
function getRoleTagText(roles: Role[] | undefined, rid: number): string {
if (!roles || !Array.isArray(roles)) {
return "未知";
}
return roles.find((role) => role.id === group_id)?.name || "未知";
return roles.find((role) => role.id === rid)?.name || "未知";
}
//

View File

@ -105,6 +105,9 @@ import { getEmailInfo, editEmailInfo, sendTestEmail } from "@/api/email";
const emailFormRef = ref<FormInstance>();
/** 是否已有数据库中的配置(允许保存时不重复填写密码) */
const hasSavedConfig = ref(false);
const emailForm = reactive({
fromAddress: "",
fromName: "",
@ -115,6 +118,18 @@ const emailForm = reactive({
timeout: 30
});
const validatePassword = (
_rule: unknown,
value: string,
callback: (e?: Error) => void
) => {
if (!hasSavedConfig.value && !String(value || "").trim()) {
callback(new Error("请输入授权码/密码"));
return;
}
callback();
};
const emailRules: FormRules = {
fromAddress: [
{ required: true, message: "请输入发件人邮箱", trigger: "blur" },
@ -122,7 +137,7 @@ const emailRules: FormRules = {
],
host: [{ required: true, message: "请输入 SMTP 主机", trigger: "blur" }],
port: [{ required: true, message: "请输入 SMTP 端口", trigger: "blur" }],
password: [{ required: true, message: "请输入授权码/密码", trigger: "blur" }]
password: [{ validator: validatePassword, trigger: "blur" }]
};
const testEmail = ref("");
@ -130,6 +145,7 @@ const testEmail = ref("");
const loadEmailConfig = async () => {
const res = await getEmailInfo();
if (res.code === 200 && Array.isArray(res.data) && res.data.length > 0) {
hasSavedConfig.value = true;
const item = res.data[0];
emailForm.fromAddress = item.from_address || "";
emailForm.fromName = item.from_name || "";
@ -138,6 +154,8 @@ const loadEmailConfig = async () => {
emailForm.password = item.password || "";
emailForm.encryption = item.encryption || "ssl";
emailForm.timeout = item.timeout != null ? item.timeout : 30;
} else {
hasSavedConfig.value = false;
}
};

View File

@ -177,8 +177,8 @@ const submitUpload = async () => {
try {
const formData = new FormData();
formData.append("file", file.raw as File);
if (props.categoryId) {
formData.append("cate", props.categoryId.toString());
if (props.categoryId !== undefined && props.categoryId !== null) {
formData.append("cate", String(props.categoryId));
}
//

View File

@ -59,7 +59,6 @@
</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">

View File

@ -23,5 +23,12 @@ export default defineConfig({
},
server: {
port: 5000,
// 开发时前端在 5000接口走相对路径 /platform/*,由这里转发到 Gogo/conf/app.conf 默认 httpport=8080
proxy: {
"/platform": {
target: "http://127.0.0.1:8080",
changeOrigin: true,
},
},
},
});