更新短信功能

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

View File

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

View File

@ -43,6 +43,7 @@
<span class="el-dropdown-link" style="cursor: pointer;"> <span class="el-dropdown-link" style="cursor: pointer;">
<img :src="getImageUrl('user')" class="user" /> <img :src="getImageUrl('user')" class="user" />
<span class="user-name">{{ displayName }}</span> <span class="user-name">{{ displayName }}</span>
<el-tag v-if="roleLabel" size="small" effect="plain" class="user-role-tag">{{ roleLabel }}</el-tag>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
@ -73,7 +74,7 @@ import { ref, computed, onMounted, onUnmounted } from "vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { useAllDataStore, useMenuStore, useTabsStore } from "@/stores"; import { useAllDataStore, useMenuStore, useTabsStore } from "@/stores";
import { useAuthStore } from "@/stores/auth"; 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 { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus'; 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(() => { const breadcrumbs = computed(() => {
@ -179,6 +191,12 @@ const displayName = computed(() => {
return user.account || ''; 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 = () => { const handleCollapse = () => {
store.state.isCollapse = !store.state.isCollapse; store.state.isCollapse = !store.state.isCollapse;
}; };
@ -424,6 +442,18 @@ onUnmounted(() => {
color: #ffffff; 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"), component: () => import("@/views/user/userProfile.vue"),
meta: { requiresAuth: true, title: "用户中心" } meta: { requiresAuth: true, title: "用户中心" }
}, },
{
path: "/system/email",
name: "SystemEmail",
component: () => import("@/views/system/email/index.vue"),
meta: { requiresAuth: true, title: "邮箱管理" }
},
// 兼容拼写错误的路径重定向 // 兼容拼写错误的路径重定向
{ {
path: "/apps/erp/dashborad", path: "/apps/erp/dashborad",

View File

@ -1,13 +1,39 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
// 用户信息类型 // 平台登录缓存仅保留以下字段(不含 tid、group_id
const defaultUser = { const defaultUser = {
id: '', id: '',
account: '', account: '',
name: '', name: '',
rid: '', 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', () => { export const useAuthStore = defineStore('auth', () => {
@ -20,8 +46,10 @@ export const useAuthStore = defineStore('auth', () => {
const cachedUser = localStorage.getItem('userInfo') const cachedUser = localStorage.getItem('userInfo')
if (cachedUser) { if (cachedUser) {
try { try {
const userInfo = JSON.parse(cachedUser) const parsed = JSON.parse(cachedUser)
Object.assign(user, userInfo) const normalized = normalizePlatformUser(parsed)
Object.assign(user, normalized)
localStorage.setItem('userInfo', JSON.stringify(normalized))
} catch (e) { } catch (e) {
console.error('Failed to parse user info from cache:', e) console.error('Failed to parse user info from cache:', e)
} }
@ -34,16 +62,8 @@ export const useAuthStore = defineStore('auth', () => {
// 保存登录信息token 和用户信息) // 保存登录信息token 和用户信息)
function setLoginInfo(loginData) { function setLoginInfo(loginData) {
const userInfo = loginData.user || 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 || '' const accessToken = loginData.token || ''
token.value = accessToken token.value = accessToken
@ -65,7 +85,7 @@ export const useAuthStore = defineStore('auth', () => {
function clearToken() { function clearToken() {
token.value = '' token.value = ''
isLoggedIn.value = false isLoggedIn.value = false
Object.assign(user, defaultUser) Object.assign(user, { ...defaultUser })
localStorage.removeItem('token') localStorage.removeItem('token')
localStorage.removeItem('userInfo') localStorage.removeItem('userInfo')
} }
@ -80,14 +100,15 @@ export const useAuthStore = defineStore('auth', () => {
} else { } else {
token.value = '' token.value = ''
isLoggedIn.value = false isLoggedIn.value = false
Object.assign(user, defaultUser) Object.assign(user, { ...defaultUser })
} }
} }
// 更新用户信息 // 更新用户信息(合并后仍只持久化平台字段)
function updateUserInfo(userInfo) { function updateUserInfo(partial) {
Object.assign(user, userInfo) const merged = normalizePlatformUser({ ...user, ...partial })
localStorage.setItem('userInfo', JSON.stringify(userInfo)) Object.assign(user, merged)
localStorage.setItem('userInfo', JSON.stringify(merged))
} }
return { return {
@ -101,4 +122,3 @@ export const useAuthStore = defineStore('auth', () => {
updateUserInfo updateUserInfo
} }
}) })

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@
/> />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="登录验证" v-if="groupId === 1" name="loginVerification"> <el-tab-pane label="登录验证" v-if="roleId === 1" name="loginVerification">
<loginVerificationSettings <loginVerificationSettings
ref="loginVerificationSettingsRef" ref="loginVerificationSettingsRef"
v-if="activeTab === 'loginVerification'" v-if="activeTab === 'loginVerification'"
@ -49,7 +49,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue"; import { ref, computed } from "vue";
import normalSettings from "./components/normalSettings.vue"; import normalSettings from "./components/normalSettings.vue";
import seoSettings from "./components/seoSettings.vue"; import seoSettings from "./components/seoSettings.vue";
import contactSettings from "./components/contactSettings.vue"; import contactSettings from "./components/contactSettings.vue";
@ -59,7 +59,7 @@ import loginVerificationSettings from "./components/loginVerification.vue";
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
const authStore = useAuthStore(); const authStore = useAuthStore();
const groupId = (authStore.userInfo as any)?.group_id; const roleId = computed(() => Number(authStore.user?.rid) || 0);
const activeTab = ref("basic"); const activeTab = ref("basic");
</script> </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> </template>
<script setup lang="ts"> <script setup lang="ts">
/**
* 租户详情内添加用户提交到 /platform/tenantUser/create
* 后端写入 yz_tenant_user租户-用户绑定含冗余账号/密码等字段
*/
import { ref, reactive } from 'vue'; import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { createTenantUser } from '@/api/tenantUser'; import { createTenantUser } from '@/api/tenantUser';
@ -60,7 +64,7 @@ const formData = reactive({
const rules = { const rules = {
account: [{ required: true, message: '请输入用户名', trigger: 'blur' }], account: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur', min: 6 }], password: [{ required: true, message: '请输入密码', trigger: 'blur', min: 5 }],
password2: [ password2: [
{ required: true, message: '请再次输入密码', trigger: 'blur' }, { required: true, message: '请再次输入密码', trigger: 'blur' },
{ {
@ -103,7 +107,12 @@ const open = (tenantId: number) => {
const submitForm = async () => { const submitForm = async () => {
if (!formRef.value) return; if (!formRef.value) return;
try {
await formRef.value.validate(); await formRef.value.validate();
} catch {
// validate reject Uncaught (in promise)
return;
}
submitting.value = true; submitting.value = true;

View File

@ -2,7 +2,7 @@
<el-drawer <el-drawer
v-model="visible" v-model="visible"
title="租户详细信息" title="租户详细信息"
size="1000px" size="1200px"
@closed="handleClosed" @closed="handleClosed"
> >
<div v-loading="loading" class="detail-container"> <div v-loading="loading" class="detail-container">
@ -40,150 +40,64 @@
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
<div class="users-section"> <el-divider />
<div class="section-title">租户用户列表</div>
<el-table :data="tenantUsers" v-loading="usersLoading" border style="width: 100%"> <div class="tenant-detail-body">
<el-table-column prop="account" label="用户名" min-width="140" align="center" /> <el-tabs
<el-table-column prop="name" label="姓名" min-width="120" align="center" /> v-model="activeTab"
<el-table-column prop="phone" label="手机号" min-width="140" align="center" /> tab-position="left"
<el-table-column prop="email" label="邮箱" min-width="180" align="center" /> class="tenant-detail-tabs"
<el-table-column prop="status" label="状态" width="90" align="center"> >
<template #default="{ row }"> <el-tab-pane label="租户用户" name="users">
<el-tag :type="Number(row.status) === 1 ? 'success' : 'danger'"> <div class="tab-pane-inner">
{{ Number(row.status) === 1 ? "启用" : "禁用" }} <TenantUsersTab :tid="detailTenantId" />
</el-tag> </div>
</template> </el-tab-pane>
</el-table-column> <!-- 后续功能在此继续增加 <el-tab-pane label="..." name="...">...</el-tab-pane> -->
<el-table-column label="操作" width="140" align="center" fixed="right"> </el-tabs>
<template #default="{ row }">
<el-button text type="primary" @click="openPasswordDialog(row)">
修改密码
</el-button>
</template>
</el-table-column>
</el-table>
</div> </div>
<div class="footer-actions"> <!-- <div class="footer-actions">
<el-button @click="visible = false">关闭</el-button> <el-button @click="visible = false">关闭</el-button>
</div> </div> -->
</div> </div>
</el-drawer> </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> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from 'vue'; import { ref } from "vue";
import { ElMessage } from "element-plus"; import { getTenantDetail } from "@/api/tenant";
import { getTenantDetail } from '@/api/tenant'; import TenantUsersTab from "./TenantUsersTab.vue";
import { getTenantUserList, editTenantUser } from "@/api/tenantUser";
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const usersLoading = ref(false);
const detailData = ref<any>({}); const detailData = ref<any>({});
const tenantUsers = ref<any[]>([]); const detailTenantId = ref<number | null>(null);
const activeTab = ref("users");
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 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) => { const open = async (id: number) => {
detailTenantId.value = id;
activeTab.value = "users";
visible.value = true; visible.value = true;
loading.value = true; loading.value = true;
usersLoading.value = true;
try { try {
const [detailRes, usersRes] = await Promise.all([ const detailRes = await getTenantDetail(id);
getTenantDetail(id),
getTenantUserList({ tid: id }),
]);
if (detailRes.code === 200) { if (detailRes.code === 200) {
detailData.value = detailRes.data; detailData.value = detailRes.data;
} }
if (usersRes?.code === 200) {
tenantUsers.value = usersRes?.data?.list || usersRes?.data || [];
} else {
tenantUsers.value = [];
}
} catch (error) { } catch (error) {
console.error('获取详情失败', error); console.error("获取详情失败", error);
tenantUsers.value = [];
} finally { } finally {
loading.value = false; loading.value = false;
usersLoading.value = false;
} }
}; };
const handleClosed = () => { const handleClosed = () => {
detailData.value = {}; detailData.value = {};
tenantUsers.value = []; detailTenantId.value = null;
activeTab.value = "users";
}; };
//
defineExpose({ open }); defineExpose({ open });
</script> </script>
@ -196,19 +110,44 @@
color: #333; color: #333;
} }
.footer-actions { .footer-actions {
margin-top: 30px; margin-top: 24px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
.users-section {
.tenant-detail-body {
margin-top: 20px; margin-top: 20px;
min-height: 0;
} }
.section-title {
margin-bottom: 12px; .tenant-detail-tabs {
font-size: 15px; width: 100%;
font-weight: 600;
color: #303133;
} }
.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) { :deep(.el-descriptions__label) {
width: 120px; width: 120px;
background-color: #f5f7fa; background-color: #f5f7fa;

View File

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

View File

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

View File

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

View File

@ -28,10 +28,10 @@
<span class="name-link" @click="handlePreview(scope.row)">{{ scope.row.name }}</span> <span class="name-link" @click="handlePreview(scope.row)">{{ scope.row.name }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="group_id" label="角色" align="center"> <el-table-column prop="rid" label="角色" align="center">
<template #default="scope"> <template #default="scope">
<el-tag :type="getRoleTagType(scope.row.group_id)" size="small"> <el-tag :type="getRoleTagType(scope.row.rid)" size="small">
{{ getRoleTagText(roles, scope.row.group_id) }} {{ getRoleTagText(roles, scope.row.rid) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@ -98,7 +98,7 @@ interface User {
phone: string; phone: string;
qq: string; qq: string;
sex: number; sex: number;
group_id: number; rid: number;
status: number; status: number;
last_login_ip: string; last_login_ip: string;
login_count: number; login_count: number;
@ -153,22 +153,22 @@ const fetchRoles = async () => {
}; };
// tag // tag
function getRoleTagType(group_id: number): string { function getRoleTagType(rid: number): string {
const typeMap: Record<number, string> = { const typeMap: Record<number, string> = {
1: "primary", 1: "primary",
2: "success", 2: "success",
3: "warning", 3: "warning",
4: "danger", 4: "danger",
}; };
return typeMap[group_id] || "primary"; return typeMap[rid] || "primary";
} }
// tag // tag
function getRoleTagText(roles: Role[] | undefined, group_id: number): string { function getRoleTagText(roles: Role[] | undefined, rid: number): string {
if (!roles || !Array.isArray(roles)) { if (!roles || !Array.isArray(roles)) {
return "未知"; 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 emailFormRef = ref<FormInstance>();
/** 是否已有数据库中的配置(允许保存时不重复填写密码) */
const hasSavedConfig = ref(false);
const emailForm = reactive({ const emailForm = reactive({
fromAddress: "", fromAddress: "",
fromName: "", fromName: "",
@ -115,6 +118,18 @@ const emailForm = reactive({
timeout: 30 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 = { const emailRules: FormRules = {
fromAddress: [ fromAddress: [
{ required: true, message: "请输入发件人邮箱", trigger: "blur" }, { required: true, message: "请输入发件人邮箱", trigger: "blur" },
@ -122,7 +137,7 @@ const emailRules: FormRules = {
], ],
host: [{ required: true, message: "请输入 SMTP 主机", trigger: "blur" }], host: [{ required: true, message: "请输入 SMTP 主机", trigger: "blur" }],
port: [{ 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(""); const testEmail = ref("");
@ -130,6 +145,7 @@ const testEmail = ref("");
const loadEmailConfig = async () => { const loadEmailConfig = async () => {
const res = await getEmailInfo(); const res = await getEmailInfo();
if (res.code === 200 && Array.isArray(res.data) && res.data.length > 0) { if (res.code === 200 && Array.isArray(res.data) && res.data.length > 0) {
hasSavedConfig.value = true;
const item = res.data[0]; const item = res.data[0];
emailForm.fromAddress = item.from_address || ""; emailForm.fromAddress = item.from_address || "";
emailForm.fromName = item.from_name || ""; emailForm.fromName = item.from_name || "";
@ -138,6 +154,8 @@ const loadEmailConfig = async () => {
emailForm.password = item.password || ""; emailForm.password = item.password || "";
emailForm.encryption = item.encryption || "ssl"; emailForm.encryption = item.encryption || "ssl";
emailForm.timeout = item.timeout != null ? item.timeout : 30; emailForm.timeout = item.timeout != null ? item.timeout : 30;
} else {
hasSavedConfig.value = false;
} }
}; };

View File

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

View File

@ -59,7 +59,6 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="create_time" label="创建时间" width="180" /> <el-table-column prop="create_time" label="创建时间" width="180" />
<el-table-column prop="update_time" label="更新时间" width="180" />
</el-table> </el-table>
<div class="pagination" v-if="total > 0"> <div class="pagination" v-if="total > 0">

View File

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