更新
This commit is contained in:
1 parent
b6b528490c
commit
4ab63e77ed
28 files changed
+1858
-695
No files matched your search
+111
-102
@@ -1,102 +1,111 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
//获取所有用户信息
|
||||
export function getAllUsers(params) {
|
||||
return request({
|
||||
url: '/backend/getAllUsers',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
//获取租户用户
|
||||
export function getTenantUsers(tenantId) {
|
||||
return request({
|
||||
url: `/backend/getTenantUsers/${tenantId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
// params.by === 'id':按 yz_system_tenant_user 主键精确查询(用户管理页传列表行 id 时必须携带);
|
||||
// 缺省时后端兼容 identity_id / 老 uid(个人中心等场景)
|
||||
export function getUserInfo(userId, params) {
|
||||
return request({
|
||||
url: `/backend/getUserInfo/${userId}`,
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 添加用户
|
||||
export function addUser(data) {
|
||||
return request({
|
||||
url: '/backend/addUser',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 编辑用户信息(params.by === 'id' 时后端按主键精确匹配)
|
||||
export function editUser(userId, data, params) {
|
||||
return request({
|
||||
url: `/backend/editUser/${userId}`,
|
||||
method: 'post',
|
||||
data,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新用户信息(编辑用户的别名)
|
||||
export function updateUserInfo(userId, data) {
|
||||
return editUser(userId, data);
|
||||
}
|
||||
|
||||
// 更新个人资料
|
||||
export function updateUserProfile(userId, data) {
|
||||
return editUser(userId, data);
|
||||
}
|
||||
|
||||
// 绑定/更换手机号
|
||||
export function bindPhone(userId, phone) {
|
||||
return editUser(userId, { phone });
|
||||
}
|
||||
|
||||
// 绑定/更换邮箱
|
||||
export function bindEmail(userId, email) {
|
||||
return editUser(userId, { email });
|
||||
}
|
||||
|
||||
// 上传并设置当前登录用户头像(一步到位:上传文件 + 写入认证中心头像)
|
||||
// formData 需包含 file 字段;返回 data.url 为头像地址
|
||||
export function uploadAvatar(formData) {
|
||||
return request({
|
||||
url: '/backend/uploadavatar',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
// 上传不设超时;不要手写 Content-Type,需浏览器生成 multipart boundary
|
||||
timeout: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除用户(params.by === 'id' 时后端按主键精确匹配)
|
||||
export function deleteUser(userId, params) {
|
||||
return request({
|
||||
url: `/backend/deleteUser/${userId}`,
|
||||
method: 'delete',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 修改密码;byID=true 表示 userId 是用户管理列表行主键,后端按主键精确匹配
|
||||
export function changePassword(userId, data, byID = false) {
|
||||
return request({
|
||||
url: '/backend/changePassword',
|
||||
method: 'post',
|
||||
data: {
|
||||
id: userId,
|
||||
password: data.newPassword,
|
||||
by: byID ? 'id' : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 将所选租户用户与人员档案进行比对,有差异时同步人员信息
|
||||
export function syncTenantUsersToEmployees(userIds) {
|
||||
return request({
|
||||
url: "/backend/syncTenantUsersToEmployees",
|
||||
method: "post",
|
||||
data: { user_ids: userIds },
|
||||
});
|
||||
}
|
||||
|
||||
//获取所有用户信息
|
||||
export function getAllUsers(params) {
|
||||
return request({
|
||||
url: '/backend/getAllUsers',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
//获取租户用户
|
||||
export function getTenantUsers(tenantId) {
|
||||
return request({
|
||||
url: `/backend/getTenantUsers/${tenantId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
// params.by === 'id':按 yz_system_tenant_user 主键精确查询(用户管理页传列表行 id 时必须携带);
|
||||
// 缺省时后端兼容 identity_id / 老 uid(个人中心等场景)
|
||||
export function getUserInfo(userId, params) {
|
||||
return request({
|
||||
url: `/backend/getUserInfo/${userId}`,
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 添加用户
|
||||
export function addUser(data) {
|
||||
return request({
|
||||
url: '/backend/addUser',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 编辑用户信息(params.by === 'id' 时后端按主键精确匹配)
|
||||
export function editUser(userId, data, params) {
|
||||
return request({
|
||||
url: `/backend/editUser/${userId}`,
|
||||
method: 'post',
|
||||
data,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新用户信息(编辑用户的别名)
|
||||
export function updateUserInfo(userId, data) {
|
||||
return editUser(userId, data);
|
||||
}
|
||||
|
||||
// 更新个人资料
|
||||
export function updateUserProfile(userId, data) {
|
||||
return editUser(userId, data);
|
||||
}
|
||||
|
||||
// 绑定/更换手机号
|
||||
export function bindPhone(userId, phone) {
|
||||
return editUser(userId, { phone });
|
||||
}
|
||||
|
||||
// 绑定/更换邮箱
|
||||
export function bindEmail(userId, email) {
|
||||
return editUser(userId, { email });
|
||||
}
|
||||
|
||||
// 上传并设置当前登录用户头像(一步到位:上传文件 + 写入认证中心头像)
|
||||
// formData 需包含 file 字段;返回 data.url 为头像地址
|
||||
export function uploadAvatar(formData) {
|
||||
return request({
|
||||
url: '/backend/uploadavatar',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
// 上传不设超时;不要手写 Content-Type,需浏览器生成 multipart boundary
|
||||
timeout: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除用户(params.by === 'id' 时后端按主键精确匹配)
|
||||
export function deleteUser(userId, params) {
|
||||
return request({
|
||||
url: `/backend/deleteUser/${userId}`,
|
||||
method: 'delete',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 修改密码;byID=true 表示 userId 是用户管理列表行主键,后端按主键精确匹配
|
||||
export function changePassword(userId, data, byID = false) {
|
||||
return request({
|
||||
url: '/backend/changePassword',
|
||||
method: 'post',
|
||||
data: {
|
||||
id: userId,
|
||||
password: data.newPassword,
|
||||
by: byID ? 'id' : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -158,16 +158,20 @@
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="msg.role === 'assistant' && (msg.tokens || msg.durationMs || msg.firstTokenMs)"
|
||||
v-if="msg.role === 'assistant' && (msg.tokens || msg.durationMs || msg.firstTokenMs || msg.model || msg.providerName || msg.provider_name)"
|
||||
class="message-meta"
|
||||
>
|
||||
<span v-if="msg.firstTokenMs" class="meta-item">首字 {{ (msg.firstTokenMs / 1000).toFixed(1) }}s</span>
|
||||
<span v-if="msg.firstTokenMs" class="meta-dot">·</span>
|
||||
<span class="meta-item">{{ formatDuration(msg.durationMs) }}</span>
|
||||
<span class="meta-dot">·</span>
|
||||
<span class="meta-item">{{ msg.tokens }} tokens</span>
|
||||
<span v-if="msg.model" class="meta-dot">·</span>
|
||||
<span v-if="msg.model" class="meta-item">{{ msg.model }}</span>
|
||||
<span v-if="msg.providerName || msg.provider_name || msg.model" class="meta-item">
|
||||
{{ msg.providerName || msg.provider_name || '未知厂商' }}<template v-if="msg.model"> · {{ msg.model }}</template>
|
||||
</span>
|
||||
<span v-if="msg.tokens || msg.durationMs" class="meta-dot">·</span>
|
||||
<span v-if="msg.durationMs" class="meta-item">用时 {{ formatDuration(msg.durationMs) }}</span>
|
||||
<span v-if="msg.durationMs && msg.tokens" class="meta-dot">·</span>
|
||||
<span v-if="msg.tokens" class="meta-item">{{ formatTokenRate(msg.tokens, msg.durationMs) }} token/s</span>
|
||||
<span v-if="msg.tokens" class="meta-dot">·</span>
|
||||
<span v-if="msg.tokens" class="meta-item">{{ msg.tokens }} tokens</span>
|
||||
</div>
|
||||
<div class="message-actions">
|
||||
<el-tooltip content="复制" placement="top">
|
||||
@@ -369,6 +373,17 @@ const currentModels = computed(() => {
|
||||
return p && p.models ? p.models : []
|
||||
})
|
||||
|
||||
const currentProvider = computed(() =>
|
||||
providerList.value.find((item) => item.id === selectedProviderId.value) || null
|
||||
)
|
||||
|
||||
function formatTokenRate(tokens, durationMs) {
|
||||
const count = Number(tokens || 0)
|
||||
const elapsed = Number(durationMs || 0)
|
||||
if (!count || !elapsed) return '—'
|
||||
return (count / (elapsed / 1000)).toFixed(1)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProviders()
|
||||
fetchDefaultPreset()
|
||||
@@ -475,7 +490,11 @@ async function fetchMessages(sessionId) {
|
||||
try {
|
||||
const res = await getChatMessageList(sessionId)
|
||||
if (res.code === 200) {
|
||||
messages.value = res.data.list || []
|
||||
messages.value = (res.data.list || []).map((msg) => ({
|
||||
...msg,
|
||||
durationMs: msg.durationMs ?? msg.duration_ms ?? 0,
|
||||
providerName: msg.providerName || msg.provider_name || '',
|
||||
}))
|
||||
scrollToBottom()
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -748,9 +767,10 @@ async function doSend(content, images = []) {
|
||||
scrollToBottom()
|
||||
} else if (eventType === 'done') {
|
||||
aiMsg.id = parsed.message_id
|
||||
if (parsed.tokens) aiMsg.tokens = parsed.tokens
|
||||
if (parsed.duration_ms) aiMsg.durationMs = parsed.duration_ms
|
||||
if (parsed.tokens !== undefined) aiMsg.tokens = parsed.tokens
|
||||
if (parsed.duration_ms !== undefined) aiMsg.durationMs = parsed.duration_ms
|
||||
if (parsed.model) aiMsg.model = parsed.model
|
||||
aiMsg.providerName = parsed.provider_name || currentProvider.value?.name || ''
|
||||
} else if (eventType === 'error') {
|
||||
streamError = new Error(parsed.error || 'AI调用失败')
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@
|
||||
>
|
||||
确认到账
|
||||
</el-button>
|
||||
<div class="toolbar-right">
|
||||
<!-- <div class="toolbar-right">
|
||||
<el-button
|
||||
v-if="isEditable"
|
||||
type="primary"
|
||||
@@ -331,7 +331,7 @@
|
||||
>
|
||||
{{ isDraft ? "提交审核" : "重新提交" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
|
||||
<el-form-item label="账号" prop="account">
|
||||
<el-input v-model="form.account" :disabled="isEdit" placeholder="留空时按组织设置自动生成" />
|
||||
<span v-if="isEdit" style="font-size: 12px; color: #909399; margin-top: 4px; display: block">
|
||||
账号与用户管理保持一致,不可直接修改
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password">
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>{{ labels.employeeLabel }}管理</h2>
|
||||
<p>{{ labels.name }}与其它模块共用同一份{{ labels.employeeLabel }}数据</p>
|
||||
<p v-if="module === 'basic'">人员数据与「用户管理」自动匹配,由系统根据用户账号自动生成</p>
|
||||
<p v-else>{{ labels.name }}与其它模块共用同一份{{ labels.employeeLabel }}数据</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Refresh" @click="loadData">刷新</el-button>
|
||||
@@ -15,7 +16,15 @@
|
||||
>
|
||||
批量调岗({{ selectedRows.length }})
|
||||
</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="editDialogRef?.open(null)">
|
||||
<el-button
|
||||
v-if="module === 'basic'"
|
||||
type="primary"
|
||||
plain
|
||||
@click="goToUsers"
|
||||
>
|
||||
前往用户管理
|
||||
</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="handleAddEmployee">
|
||||
新增{{ labels.employeeLabel }}
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -89,7 +98,7 @@
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="viewDialogRef?.open(row)">查看</el-button>
|
||||
<el-button link type="primary" size="small" @click="editDialogRef?.open(row)">编辑</el-button>
|
||||
<el-button link type="warning" size="small" @click="editDialogRef?.open(row, { copy: true })">复制</el-button>
|
||||
<el-button v-if="module !== 'basic'" link type="warning" size="small" @click="editDialogRef?.open(row, { copy: true })">复制</el-button>
|
||||
<el-button link type="primary" size="small" @click="moveDialogRef?.open([row])">调岗</el-button>
|
||||
<el-button link type="warning" size="small" @click="handleResetPassword(row)">重置密码</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
@@ -141,6 +150,8 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus, Refresh, Search } from '@element-plus/icons-vue';
|
||||
import EmployeeEditDialog from './EmployeeEditDialog.vue';
|
||||
@@ -167,6 +178,33 @@ const editDialogRef = ref();
|
||||
const viewDialogRef = ref();
|
||||
const moveDialogRef = ref();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const goToUsers = () => {
|
||||
router.push('/basicSettings/users');
|
||||
};
|
||||
|
||||
const handleAddEmployee = () => {
|
||||
if (props.module === 'basic') {
|
||||
ElMessageBox.confirm(
|
||||
'人员与「用户管理」保持一致,系统在添加用户后会自动生成对应人员档案。\n\n如需添加新人员,请前往「用户管理」添加用户账号。',
|
||||
'新增人员说明',
|
||||
{
|
||||
confirmButtonText: '前往用户管理',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info',
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
router.push('/basicSettings/users');
|
||||
})
|
||||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
editDialogRef.value?.open(null);
|
||||
};
|
||||
|
||||
|
||||
const rows = ref([]);
|
||||
const selectedRows = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" :title="dialogTitle" width="500px">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-alert
|
||||
v-if="isAdd"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="添加用户后,系统将自动在「人员管理」中生成对应员工档案,可在人员管理中配置部门与职位。"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
<div class="form-title">账号信息</div>
|
||||
<!-- 账号 -->
|
||||
<el-form-item label="账号">
|
||||
|
||||
@@ -1,367 +1,442 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>
|
||||
用户管理
|
||||
<el-tag v-if="quotaInfo" size="small" :type="quotaFull ? 'danger' : 'info'" effect="plain"
|
||||
class="quota-tag">
|
||||
用户数 {{ quotaInfo.used }} / {{ quotaInfo.quota }}
|
||||
</el-tag>
|
||||
</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-alert v-if="quotaFull" type="warning" show-icon :closable="false" class="quota-alert"
|
||||
title="用户数已达上限,新增账号前请联系平台管理员增购用户数" />
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-form inline>
|
||||
<el-form-item label="租户筛选">
|
||||
<el-select v-model="selectedTenantId" style="width: 220px" disabled>
|
||||
<el-option :label="`当前租户(ID: ${selectedTenantId || '-'})`" :value="selectedTenantId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<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="name" label="姓名" min-width="120" 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="group_id" label="角色" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getRoleTagType(scope.row.group_id)" size="small">
|
||||
{{ getRoleTagText(roles, scope.row.group_id) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="account" label="账号" min-width="180" align="center" />
|
||||
<el-table-column prop="phone" label="手机号" min-width="180" align="center" />
|
||||
<el-table-column prop="qq" label="QQ" min-width="180" align="center" />
|
||||
<el-table-column prop="last_login_ip" label="最后登录IP" width="120" align="center" />
|
||||
<el-table-column prop="login_count" label="登陆次数" width="120" 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 v-if="scope.row.username !== 'admin' && scope.row.id !== 1" 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, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||
import { getTenantUsers, deleteUser } from "@/api/user";
|
||||
import { getTenantQuotaInfo } from "@/api/tenant";
|
||||
import { getAllRoles } from "@/api/role";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import UserEditDialog from "./components/userEdit.vue";
|
||||
import ChangePasswordDialog from "./components/changePassword.vue";
|
||||
import PreviewDialog from "./components/preview.vue";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
tid?: number;
|
||||
account: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
qq: 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;
|
||||
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 authStore = useAuthStore();
|
||||
const selectedTenantId = ref<number>(Number(authStore.user.tid || 0));
|
||||
|
||||
// 组件引用
|
||||
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);
|
||||
|
||||
// 用户数配额(由平台端配置:默认 20,可增购)
|
||||
const quotaInfo = ref<any>(null);
|
||||
const quotaFull = computed(
|
||||
() =>
|
||||
!!quotaInfo.value &&
|
||||
Number(quotaInfo.value.quota) > 0 &&
|
||||
Number(quotaInfo.value.used) >= Number(quotaInfo.value.quota)
|
||||
);
|
||||
|
||||
const fetchQuotaInfo = async () => {
|
||||
try {
|
||||
const res = await getTenantQuotaInfo();
|
||||
if (res?.code === 200) {
|
||||
quotaInfo.value = res.data || null;
|
||||
}
|
||||
} catch {
|
||||
quotaInfo.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
//刷新
|
||||
const refresh = async () => {
|
||||
await fetchUsers();
|
||||
fetchQuotaInfo();
|
||||
};
|
||||
|
||||
// 获取角色列表
|
||||
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";
|
||||
}
|
||||
|
||||
// 获取角色tag文本
|
||||
function getRoleTagText(roles: Role[] | undefined, group_id: number): string {
|
||||
if (!roles || !Array.isArray(roles)) {
|
||||
return "未知";
|
||||
}
|
||||
return roles.find((role) => role.id === group_id)?.name || "未知";
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
if (!selectedTenantId.value) {
|
||||
users.value = [];
|
||||
total.value = 0;
|
||||
return;
|
||||
}
|
||||
const res = await getTenantUsers(selectedTenantId.value);
|
||||
users.value = res?.data?.list || [];
|
||||
total.value = Number(res?.data?.total || 0);
|
||||
} catch (e) {
|
||||
users.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加用户
|
||||
const handleAddUser = () => {
|
||||
// 用户数达到上限时不允许再新增,提示联系平台增购
|
||||
if (quotaFull.value) {
|
||||
ElMessageBox.alert(
|
||||
`当前租户用户数已达上限(${quotaInfo.value.used}/${quotaInfo.value.quota}),请联系平台管理员增购用户数。`,
|
||||
"用户数已达上限",
|
||||
{ type: "warning", confirmButtonText: "我知道了" }
|
||||
).catch(() => {});
|
||||
return;
|
||||
}
|
||||
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();
|
||||
fetchQuotaInfo();
|
||||
};
|
||||
|
||||
// 密码修改成功回调
|
||||
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, { by: "id" });
|
||||
ElMessage.success("删除成功");
|
||||
fetchUsers();
|
||||
fetchQuotaInfo();
|
||||
} catch (e) {
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
selectedTenantId.value = Number(authStore.user.tid || 0);
|
||||
fetchUsers();
|
||||
fetchRoles();
|
||||
fetchQuotaInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.filter-bar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.quota-tag {
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.quota-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.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>
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>
|
||||
用户管理
|
||||
<el-tag v-if="quotaInfo" size="small" :type="quotaFull ? 'danger' : 'info'" effect="plain"
|
||||
class="quota-tag">
|
||||
用户数 {{ quotaInfo.used }} / {{ quotaInfo.quota }}
|
||||
</el-tag>
|
||||
</h2>
|
||||
<div class="header-actions">
|
||||
<el-button
|
||||
type="success"
|
||||
:disabled="!selectedUsers.length || syncingEmployees"
|
||||
:loading="syncingEmployees"
|
||||
@click="handleSyncEmployees"
|
||||
>
|
||||
同步员工{{ selectedUsers.length ? `(${selectedUsers.length})` : '' }}
|
||||
</el-button>
|
||||
<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-alert v-if="quotaFull" type="warning" show-icon :closable="false" class="quota-alert"
|
||||
title="用户数已达上限,新增账号前请联系平台管理员增购用户数" />
|
||||
|
||||
<el-alert
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
title="人员联动提示:新增用户后系统会自动生成员工;存量用户请勾选后点击「同步员工」,系统会按当前租户比对,只补建或更新有差异的员工资料。"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-form inline>
|
||||
<el-form-item label="租户筛选">
|
||||
<el-select v-model="selectedTenantId" style="width: 220px" disabled>
|
||||
<el-option :label="`当前租户(ID: ${selectedTenantId || '-'})`" :value="selectedTenantId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<el-table
|
||||
ref="userTableRef"
|
||||
:data="users"
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="48" fixed="left" />
|
||||
<el-table-column prop="id" label="ID" align="center" fixed="left" />
|
||||
<el-table-column prop="name" label="姓名" min-width="120" 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="group_id" label="角色" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getRoleTagType(scope.row.group_id)" size="small">
|
||||
{{ getRoleTagText(roles, scope.row.group_id) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="account" label="账号" min-width="180" align="center" />
|
||||
<el-table-column prop="phone" label="手机号" min-width="180" align="center" />
|
||||
<el-table-column prop="qq" label="QQ" min-width="180" align="center" />
|
||||
<el-table-column prop="last_login_ip" label="最后登录IP" width="120" align="center" />
|
||||
<el-table-column prop="login_count" label="登陆次数" width="120" 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 v-if="scope.row.username !== 'admin' && scope.row.id !== 1" 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, computed, onMounted } from "vue";
|
||||
import type { ElTable } from "element-plus";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||
import { getTenantUsers, deleteUser, syncTenantUsersToEmployees } from "@/api/user";
|
||||
import { getTenantQuotaInfo } from "@/api/tenant";
|
||||
import { getAllRoles } from "@/api/role";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import UserEditDialog from "./components/userEdit.vue";
|
||||
import ChangePasswordDialog from "./components/changePassword.vue";
|
||||
import PreviewDialog from "./components/preview.vue";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
tid?: number;
|
||||
account: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
qq: 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;
|
||||
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 authStore = useAuthStore();
|
||||
const selectedTenantId = ref<number>(Number(authStore.user.tid || 0));
|
||||
|
||||
// 组件引用
|
||||
const userEditRef = ref();
|
||||
const changePasswordRef = ref();
|
||||
const previewDialogRef = ref();
|
||||
|
||||
// 编辑/密码对话框状态
|
||||
const editDialogVisible = ref(false);
|
||||
const userTableRef = ref<InstanceType<typeof ElTable>>();
|
||||
const selectedUsers = ref<User[]>([]);
|
||||
const syncingEmployees = 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);
|
||||
|
||||
// 用户数配额(由平台端配置:默认 20,可增购)
|
||||
const quotaInfo = ref<any>(null);
|
||||
const quotaFull = computed(
|
||||
() =>
|
||||
!!quotaInfo.value &&
|
||||
Number(quotaInfo.value.quota) > 0 &&
|
||||
Number(quotaInfo.value.used) >= Number(quotaInfo.value.quota)
|
||||
);
|
||||
|
||||
const handleSelectionChange = (rows: User[]) => {
|
||||
selectedUsers.value = rows;
|
||||
};
|
||||
|
||||
const handleSyncEmployees = async () => {
|
||||
if (!selectedUsers.value.length || syncingEmployees.value) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`将按当前租户核对所选 ${selectedUsers.value.length} 个用户的员工档案;只新增缺失记录或更新存在差异的资料。已配置的部门、职位等员工专属信息会保留。继续同步吗?`,
|
||||
"同步员工",
|
||||
{ confirmButtonText: "开始同步", cancelButtonText: "取消", type: "warning" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
syncingEmployees.value = true;
|
||||
try {
|
||||
const res = await syncTenantUsersToEmployees(selectedUsers.value.map((user) => user.id));
|
||||
if (res?.code !== 200) throw new Error(res?.msg || "同步失败");
|
||||
const result = res.data || {};
|
||||
const successCount =
|
||||
Number(result.created || 0) + Number(result.updated || 0) + Number(result.unchanged || 0);
|
||||
const failedCount = Number(result.failed || 0);
|
||||
const summary = `同步完成:成功 ${successCount} 人,失败 ${failedCount} 人`;
|
||||
const errors = Array.isArray(result.errors) ? result.errors.filter(Boolean) : [];
|
||||
if (errors.length) {
|
||||
await ElMessageBox.alert(errors.join("\n"), summary, {
|
||||
confirmButtonText: "知道了",
|
||||
type: "warning",
|
||||
});
|
||||
} else {
|
||||
ElMessage.success(summary);
|
||||
}
|
||||
userTableRef.value?.clearSelection();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.msg || error?.message || "同步失败");
|
||||
} finally {
|
||||
syncingEmployees.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const fetchQuotaInfo = async () => {
|
||||
try {
|
||||
const res = await getTenantQuotaInfo();
|
||||
if (res?.code === 200) {
|
||||
quotaInfo.value = res.data || null;
|
||||
}
|
||||
} catch {
|
||||
quotaInfo.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
//刷新
|
||||
const refresh = async () => {
|
||||
await fetchUsers();
|
||||
fetchQuotaInfo();
|
||||
};
|
||||
|
||||
// 获取角色列表
|
||||
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";
|
||||
}
|
||||
|
||||
// 获取角色tag文本
|
||||
function getRoleTagText(roles: Role[] | undefined, group_id: number): string {
|
||||
if (!roles || !Array.isArray(roles)) {
|
||||
return "未知";
|
||||
}
|
||||
return roles.find((role) => role.id === group_id)?.name || "未知";
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
if (!selectedTenantId.value) {
|
||||
users.value = [];
|
||||
total.value = 0;
|
||||
return;
|
||||
}
|
||||
const res = await getTenantUsers(selectedTenantId.value);
|
||||
users.value = res?.data?.list || [];
|
||||
selectedUsers.value = [];
|
||||
userTableRef.value?.clearSelection();
|
||||
total.value = Number(res?.data?.total || 0);
|
||||
} catch (e) {
|
||||
users.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加用户
|
||||
const handleAddUser = () => {
|
||||
// 用户数达到上限时不允许再新增,提示联系平台增购
|
||||
if (quotaFull.value) {
|
||||
ElMessageBox.alert(
|
||||
`当前租户用户数已达上限(${quotaInfo.value.used}/${quotaInfo.value.quota}),请联系平台管理员增购用户数。`,
|
||||
"用户数已达上限",
|
||||
{ type: "warning", confirmButtonText: "我知道了" }
|
||||
).catch(() => {});
|
||||
return;
|
||||
}
|
||||
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();
|
||||
fetchQuotaInfo();
|
||||
};
|
||||
|
||||
// 密码修改成功回调
|
||||
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, { by: "id" });
|
||||
ElMessage.success("删除成功");
|
||||
fetchUsers();
|
||||
fetchQuotaInfo();
|
||||
} catch (e) {
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
selectedTenantId.value = Number(authStore.user.tid || 0);
|
||||
fetchUsers();
|
||||
fetchRoles();
|
||||
fetchQuotaInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.filter-bar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.quota-tag {
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.quota-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.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>
|
||||
Reference in new issue
Block a user