更新
This commit is contained in:
parent
f9609403b5
commit
2faa82f4be
33
f/web-kboss/src/api/roleManagement.js
Normal file
33
f/web-kboss/src/api/roleManagement.js
Normal file
@ -0,0 +1,33 @@
|
||||
import request from '@/utils/request'
|
||||
// 获取角色
|
||||
export function getRoleAPI(data) {
|
||||
return request({
|
||||
url: `/role/getRole.dspy`,
|
||||
method: 'get',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
// 添加角色
|
||||
export function addRoleAPI(data) {
|
||||
return request({
|
||||
url: `/reseller/reseller_add_user.dspy`,
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
// 列表
|
||||
export function getListAPI(data) {
|
||||
return request({
|
||||
url: `/user/get_user_and_role.dspy`,
|
||||
method: 'get',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
// 分配角色 参数为userid roleid
|
||||
export function assignRoleAPI(data) {
|
||||
return request({
|
||||
url: `/user/add_user_role.dspy`,
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
@ -2305,6 +2305,13 @@ export const asyncRoutes = [
|
||||
path: "/superAdministrator", component: Layout, redirect: "/superAdministrator/index", meta: {
|
||||
title: "超级管理员", icon: "el-icon-user-solid", noCache: true, fullPath: "/superAdministrator",
|
||||
}, children: [{
|
||||
path: "roleManagement",
|
||||
component: () => import("@/views/superAdministrator/roleManagement"),
|
||||
name: "RoleManagement",
|
||||
meta: {
|
||||
title: "角色管理", fullPath: "/superAdministrator/roleManagement",
|
||||
},
|
||||
}, {
|
||||
path: "addAdmin", component: () => import("@/views/superAdministrator/addAdmin"), name: "addAdmin", meta: {
|
||||
title: "添加业主机构管理员", fullPath: "/superAdministrator/addAdmin",
|
||||
},
|
||||
|
||||
@ -7,6 +7,7 @@ const MOBILE_UA_REGEXP = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Ope
|
||||
const CUSTOMER_ROLE = '客户';
|
||||
const OPERATION_ROLE = '运营';
|
||||
const FINANCE_ROLE = '财务';
|
||||
const ADMINISTRATOR_ROLE = '管理员';
|
||||
|
||||
// 这个用户能看到订单管理里的特殊子菜单,比如历史订单和订单详情。
|
||||
const SPECIAL_ORDER_USER = 'ZhipuHZ';
|
||||
@ -23,6 +24,8 @@ const OPERATION_EXTRA_ROUTE_PATHS = ['/modelManagement', '/modelInfoConfig', '/o
|
||||
// 财务角色需要额外补出来的菜单。
|
||||
const FINANCE_EXTRA_ROUTE_PATHS = ['/financialOverview'];
|
||||
|
||||
const ADMINISTRATOR_ROUTE_FULL_PATH = '/superAdministrator/roleManagement';
|
||||
|
||||
// 普通客户账号默认要补出来的基础菜单。
|
||||
const BASE_USER_ROUTE_PATHS = ['/orderManagement', '/resourceManagement'];
|
||||
|
||||
@ -350,6 +353,30 @@ function addFinanceRoutes(accessedRoutes, routes, userRoles = [], deviceType = '
|
||||
return appendMissingRoutes(accessedRoutes, getRoutesByPath(routes, FINANCE_EXTRA_ROUTE_PATHS));
|
||||
}
|
||||
|
||||
function getAdministratorRoutes(routes, deviceType = 'pc') {
|
||||
if (deviceType !== 'pc') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const superAdminRoute = findRouteByPath(routes, SUPER_ADMIN_ROUTE_PATH);
|
||||
if (!superAdminRoute) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const clonedRoute = cloneRoute(superAdminRoute);
|
||||
clonedRoute.redirect = ADMINISTRATOR_ROUTE_FULL_PATH;
|
||||
clonedRoute.children = (clonedRoute.children || []).filter(child => child.meta?.fullPath === ADMINISTRATOR_ROUTE_FULL_PATH);
|
||||
return clonedRoute.children.length ? [clonedRoute] : [];
|
||||
}
|
||||
|
||||
function addAdministratorRoutes(accessedRoutes, routes, userRoles = [], deviceType = 'pc') {
|
||||
if (!userRoles.includes(ADMINISTRATOR_ROLE)) {
|
||||
return accessedRoutes;
|
||||
}
|
||||
|
||||
return appendMissingRoutes(accessedRoutes, getAdministratorRoutes(routes, deviceType));
|
||||
}
|
||||
|
||||
// token市集是公共菜单,所有登录用户都要能看到。
|
||||
function addCommonRoutes(accessedRoutes, routes, deviceType = 'pc') {
|
||||
const commonRoutes = getRoutesByPath(routes, COMMON_ROUTE_PATHS)
|
||||
@ -495,6 +522,13 @@ const actions = {
|
||||
console.log("用户类型:", userType, "orgType:", orgType, "设备类型:", deviceType);
|
||||
console.log("ACTION generateRoutes - auths:", auths);
|
||||
|
||||
if (!isSuperAdmin && userRoles.includes(ADMINISTRATOR_ROLE)) {
|
||||
const administratorRoutes = getAdministratorRoutes(asyncRoutes, deviceType);
|
||||
commit("SET_ROUTES", administratorRoutes);
|
||||
resolve(administratorRoutes);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 先生成第一版菜单:超级管理员只拿超管菜单,普通用户按后端 auths 过滤。
|
||||
let accessedRoutes = isSuperAdmin
|
||||
? getSuperAdminRoutes(deviceType)
|
||||
@ -502,6 +536,7 @@ const actions = {
|
||||
|
||||
// 4. token市集是公共入口,所有登录用户都补上。
|
||||
accessedRoutes = addCommonRoutes(accessedRoutes, asyncRoutes, deviceType);
|
||||
accessedRoutes = addAdministratorRoutes(accessedRoutes, asyncRoutes, userRoles, deviceType);
|
||||
|
||||
if (!isSuperAdmin) {
|
||||
// 5. 普通用户再补一些固定入口,比如订单、资源、客户专属菜单。
|
||||
|
||||
@ -382,6 +382,11 @@ export default {
|
||||
})
|
||||
},
|
||||
redirectAfterLogin(res) {
|
||||
if (res.roles && res.roles.includes('管理员')) {
|
||||
sessionStorage.removeItem('loginRedirectPath')
|
||||
this.$router.push('/superAdministrator/roleManagement').catch(() => {})
|
||||
return
|
||||
}
|
||||
const redirectPath = sessionStorage.getItem('loginRedirectPath')
|
||||
if (redirectPath && redirectPath.startsWith('/') && !redirectPath.includes('/login')) {
|
||||
sessionStorage.removeItem('loginRedirectPath')
|
||||
|
||||
@ -544,6 +544,7 @@ export default Vue.extend({
|
||||
else if (role.includes('运维')) this.$router.push('/operationAndMaintenance/workOrderProcessing')
|
||||
else if (role.includes('销售')) this.$router.push('/sales/distributorManagement')
|
||||
else if (role.includes('财务')) this.$router.push('/finance/supplierSettlementStatistics')
|
||||
else if (role.includes('管理员')) this.$router.push('/superAdministrator/roleManagement')
|
||||
else if (role.includes('admin')) this.$router.push('/superAdministrator/addAdmin')
|
||||
},
|
||||
async logout() {
|
||||
|
||||
@ -243,7 +243,7 @@ export default {
|
||||
this.stopObserveEditorFullscreen()
|
||||
this.editorFullscreen = false
|
||||
},
|
||||
methods: {
|
||||
methods: {
|
||||
createEmptyForm() {
|
||||
// 创建新增文章时使用的默认表单结构。
|
||||
return {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<i class="el-icon-arrow-left"></i>
|
||||
返回
|
||||
</button>
|
||||
<span>API文档</span>
|
||||
<span>元境 API 文档</span>
|
||||
</header>
|
||||
|
||||
<main class="doc-container">
|
||||
@ -19,40 +19,12 @@
|
||||
></el-alert>
|
||||
|
||||
<section class="doc-hero">
|
||||
<h1>{{ apiDoc.model_name }} API 文档</h1>
|
||||
<p>{{ heroDescription }}</p>
|
||||
<!-- <div class="quick-tabs">
|
||||
<span v-for="item in quickTabs" :key="item">{{ item }}</span>
|
||||
</div> -->
|
||||
<h1>元境 API 文档</h1>
|
||||
<p>统一展示大模型、视频、图像、音频、任务查询等接口说明。</p>
|
||||
</section>
|
||||
|
||||
<section v-loading="loading" class="doc-section">
|
||||
<h2>1. 接口地址</h2>
|
||||
<p>统一使用 HTTPS 请求,所有接口都需要携带平台签发的 API Key。</p>
|
||||
<pre><code>{{ apiUrlText }}</code></pre>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<section class="doc-section">
|
||||
<h2>2. 请求示例</h2>
|
||||
<pre><code>{{ apiDoc.curl_code || requestExample }}</code></pre>
|
||||
</section>
|
||||
|
||||
<section class="doc-section">
|
||||
<h2>3. Python 示例</h2>
|
||||
<pre><code>{{ apiDoc.python_code || pythonExample }}</code></pre>
|
||||
</section>
|
||||
|
||||
<section class="doc-section">
|
||||
<h2>4. 错误码</h2>
|
||||
<el-table :data="errorCodes" border size="small">
|
||||
<el-table-column prop="code" label="错误码" width="140"></el-table-column>
|
||||
<el-table-column prop="message" label="说明"></el-table-column>
|
||||
<el-table-column prop="suggestion" label="处理建议"></el-table-column>
|
||||
</el-table>
|
||||
<section v-loading="loading" class="doc-section markdown-section">
|
||||
<div v-html="renderedMarkdown"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@ -61,124 +33,137 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reqModelApiDocument } from '@/api/model/model'
|
||||
|
||||
export default {
|
||||
name: 'ApiDocument',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
errorMessage: '',
|
||||
apiDoc: {
|
||||
id: '',
|
||||
api_url: '',
|
||||
model_id: '',
|
||||
curl_code: '',
|
||||
python_code: '',
|
||||
model_name: '模型'
|
||||
},
|
||||
quickTabs: ['API概览', '认证方式', '请求参数', '代码示例', '错误码'],
|
||||
capabilityTable: [
|
||||
{ name: '文本对话', value: '支持单轮和多轮对话生成', status: '支持' },
|
||||
{ name: '流式输出', value: '通过 stream=true 开启 SSE 增量返回', status: '支持' },
|
||||
{ name: 'Function Call', value: '支持工具调用和结构化参数', status: '支持' },
|
||||
{ name: '图像输入', value: '可在 messages 中传入图片内容', status: '支持' },
|
||||
{ name: '私有化部署', value: '当前公共服务暂不支持私有化', status: '暂不支持' }
|
||||
],
|
||||
requestParams: [
|
||||
{ name: 'model', type: 'string', required: '是', desc: '模型 ID,例如 minimax-m2.5' },
|
||||
{ name: 'messages', type: 'array', required: '是', desc: '对话消息列表,包含 role 和 content' },
|
||||
{ name: 'temperature', type: 'number', required: '否', desc: '采样温度,数值越高输出越随机' },
|
||||
{ name: 'stream', type: 'boolean', required: '否', desc: '是否开启流式返回' },
|
||||
{ name: 'max_tokens', type: 'number', required: '否', desc: '限制模型最大输出长度' }
|
||||
],
|
||||
errorCodes: [
|
||||
{ code: '401', message: '认证失败', suggestion: '检查 API Key 是否正确或过期' },
|
||||
{ code: '429', message: '请求过于频繁', suggestion: '降低并发或等待限流恢复' },
|
||||
{ code: '500', message: '服务异常', suggestion: '稍后重试或联系平台支持' }
|
||||
],
|
||||
requestExample: `curl https://api.kboss.example.com/v2/chat/completions \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "Authorization: Bearer $KBOSS_API_KEY" \\
|
||||
-d '{
|
||||
"model": "minimax-m2.5",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "帮我写一段模型上架介绍"
|
||||
}
|
||||
],
|
||||
"temperature": 0.7,
|
||||
"stream": false
|
||||
}'`,
|
||||
pythonExample: `import requests
|
||||
|
||||
url = "https://api.kboss.example.com/v2/chat/completions"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer $KBOSS_API_KEY"
|
||||
}
|
||||
data = {
|
||||
"model": "minimax-m2.5",
|
||||
"messages": [{"role": "user", "content": "Hello!"}],
|
||||
"stream": False
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
print(response.json())`
|
||||
markdownText: '',
|
||||
markdownUrl: 'https://token.opencomputing.cn/docs/api_doc_zh.md'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
apiUrlText() {
|
||||
const curlUrl = this.getUrlFromCurl(this.apiDoc.curl_code)
|
||||
const url = this.apiDoc.api_url || curlUrl
|
||||
return url ? `POST ${url}` : '接口地址以后端返回为准'
|
||||
},
|
||||
heroDescription() {
|
||||
const modelName = this.apiDoc.model_name || '当前模型'
|
||||
return `${modelName} 接口调用示例,包含 curl 与 Python 两种接入方式。`
|
||||
renderedMarkdown() {
|
||||
return this.renderMarkdown(this.markdownText)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchApiDocument()
|
||||
},
|
||||
watch: {
|
||||
'$route.query.id'() {
|
||||
this.fetchApiDocument()
|
||||
}
|
||||
this.fetchMarkdownDocument()
|
||||
},
|
||||
methods: {
|
||||
// 通过路由传入的 id 获取模型 API 文档数据。
|
||||
async fetchApiDocument() {
|
||||
const id = this.$route.query.id || this.$route.query.model_id
|
||||
if (!id) {
|
||||
this.errorMessage = '缺少模型ID,无法获取 API 文档'
|
||||
return
|
||||
}
|
||||
|
||||
async fetchMarkdownDocument() {
|
||||
this.loading = true
|
||||
this.errorMessage = ''
|
||||
try {
|
||||
const res = await reqModelApiDocument({ id })
|
||||
if (res && res.status && res.data) {
|
||||
this.apiDoc = {
|
||||
...this.apiDoc,
|
||||
...res.data,
|
||||
model_name: res.data.model_name || this.apiDoc.model_name
|
||||
}
|
||||
return
|
||||
const response = await fetch(this.markdownUrl)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
this.errorMessage = (res && res.msg) || 'API 文档数据获取失败'
|
||||
this.markdownText = await response.text()
|
||||
} catch (error) {
|
||||
console.error('[API文档] 获取模型 API 文档失败', error)
|
||||
this.errorMessage = 'API 文档数据获取失败,请稍后重试'
|
||||
console.error('[API文档] 获取 Markdown 文档失败', error)
|
||||
this.errorMessage = 'API 文档加载失败,请稍后重试'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
getUrlFromCurl(curlCode) {
|
||||
if (!curlCode) return ''
|
||||
const match = String(curlCode).match(/https?:\/\/[^\s\\]+/)
|
||||
return match ? match[0] : ''
|
||||
escapeHtml(text = '') {
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
},
|
||||
renderInline(text = '') {
|
||||
return this.escapeHtml(text)
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
},
|
||||
renderTable(lines) {
|
||||
const rows = lines
|
||||
.filter(line => line.trim().startsWith('|'))
|
||||
.map(line => line.trim().replace(/^\||\|$/g, '').split('|').map(cell => this.renderInline(cell.trim())))
|
||||
if (!rows.length) return ''
|
||||
const header = rows[0] || []
|
||||
const bodyRows = rows.slice(2)
|
||||
return `<div class="md-table-wrap"><table><thead><tr>${header.map(cell => `<th>${cell}</th>`).join('')}</tr></thead><tbody>${bodyRows.map(row => `<tr>${row.map(cell => `<td>${cell}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`
|
||||
},
|
||||
renderMarkdown(markdown = '') {
|
||||
const lines = markdown.replace(/\r\n/g, '\n').split('\n')
|
||||
const html = []
|
||||
let index = 0
|
||||
|
||||
while (index < lines.length) {
|
||||
const line = lines[index]
|
||||
const trimmed = line.trim()
|
||||
|
||||
if (!trimmed) {
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('```')) {
|
||||
const lang = trimmed.replace(/```/, '').trim()
|
||||
const code = []
|
||||
index += 1
|
||||
while (index < lines.length && !lines[index].trim().startsWith('```')) {
|
||||
code.push(lines[index])
|
||||
index += 1
|
||||
}
|
||||
html.push(`<pre><code class="language-${this.escapeHtml(lang)}">${this.escapeHtml(code.join('\n'))}</code></pre>`)
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('|')) {
|
||||
const tableLines = []
|
||||
while (index < lines.length && lines[index].trim().startsWith('|')) {
|
||||
tableLines.push(lines[index])
|
||||
index += 1
|
||||
}
|
||||
html.push(this.renderTable(tableLines))
|
||||
continue
|
||||
}
|
||||
|
||||
if (/^---+$/.test(trimmed)) {
|
||||
html.push('<hr>')
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
const heading = trimmed.match(/^(#{1,6})\s+(.*)$/)
|
||||
if (heading) {
|
||||
const level = heading[1].length
|
||||
html.push(`<h${level}>${this.renderInline(heading[2])}</h${level}>`)
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (/^[-*]\s+/.test(trimmed)) {
|
||||
const items = []
|
||||
while (index < lines.length && /^[-*]\s+/.test(lines[index].trim())) {
|
||||
items.push(lines[index].trim().replace(/^[-*]\s+/, ''))
|
||||
index += 1
|
||||
}
|
||||
html.push(`<ul>${items.map(item => `<li>${this.renderInline(item)}</li>`).join('')}</ul>`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (/^\d+\.\s+/.test(trimmed)) {
|
||||
const items = []
|
||||
while (index < lines.length && /^\d+\.\s+/.test(lines[index].trim())) {
|
||||
items.push(lines[index].trim().replace(/^\d+\.\s+/, ''))
|
||||
index += 1
|
||||
}
|
||||
html.push(`<ol>${items.map(item => `<li>${this.renderInline(item)}</li>`).join('')}</ol>`)
|
||||
continue
|
||||
}
|
||||
|
||||
html.push(`<p>${this.renderInline(trimmed)}</p>`)
|
||||
index += 1
|
||||
}
|
||||
|
||||
return html.join('')
|
||||
},
|
||||
goBack() {
|
||||
this.$router.back()
|
||||
@ -275,8 +260,103 @@ print(response.json())`
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-section {
|
||||
line-height: 1.8;
|
||||
|
||||
::v-deep h1 {
|
||||
margin: 0 0 24px;
|
||||
color: #111827;
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
::v-deep h2 {
|
||||
margin: 34px 0 16px;
|
||||
padding-left: 10px;
|
||||
color: #1f2d3d;
|
||||
font-size: 22px;
|
||||
border-left: 3px solid #2f6bff;
|
||||
}
|
||||
|
||||
::v-deep h3 {
|
||||
margin: 26px 0 12px;
|
||||
color: #1f2937;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
::v-deep h4,
|
||||
::v-deep h5 {
|
||||
margin: 20px 0 10px;
|
||||
color: #334155;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
::v-deep p,
|
||||
::v-deep li {
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
::v-deep ul,
|
||||
::v-deep ol {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
::v-deep hr {
|
||||
height: 1px;
|
||||
margin: 28px 0;
|
||||
background: #e5e7eb;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
::v-deep code {
|
||||
padding: 2px 6px;
|
||||
color: #d6336c;
|
||||
background: #fff1f2;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::v-deep pre code {
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
::v-deep .md-table-wrap {
|
||||
width: 100%;
|
||||
margin: 16px 0 24px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::v-deep table {
|
||||
width: 100%;
|
||||
min-width: 680px;
|
||||
border-collapse: collapse;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
::v-deep th,
|
||||
::v-deep td {
|
||||
padding: 10px 12px;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
::v-deep th {
|
||||
color: #1f2937;
|
||||
font-weight: 700;
|
||||
background: #f8fafc;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
margin: 14px 0 24px;
|
||||
padding: 16px;
|
||||
color: #e6edf3;
|
||||
overflow-x: auto;
|
||||
|
||||
@ -0,0 +1,619 @@
|
||||
<template>
|
||||
<div class="role-management-page">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<p class="eyebrow">ROLE MANAGEMENT</p>
|
||||
<h1>角色管理</h1>
|
||||
<span>统一管理系统用户与角色权限。</span>
|
||||
</div>
|
||||
<el-button type="primary" icon="el-icon-plus" class="add-role-btn" @click="openAddDialog">
|
||||
添加角色
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="filter-card">
|
||||
<el-form :model="queryForm" inline class="filter-form">
|
||||
<el-form-item label="用户名">
|
||||
<el-input
|
||||
v-model.trim="queryForm.username"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="请输入用户名"
|
||||
@keyup.enter.native="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input
|
||||
v-model.trim="queryForm.mobile"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="请输入手机号"
|
||||
@keyup.enter.native="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button size="small" icon="el-icon-search" type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button size="small" icon="el-icon-refresh-left" @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<el-table v-loading="loading" :data="roles" class="role-table" empty-text="暂无角色数据">
|
||||
<el-table-column label="序号" width="80" align="center">
|
||||
<template slot-scope="scope">{{ scope.$index + 1 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="username" label="用户名" min-width="180">
|
||||
<template slot-scope="scope">
|
||||
<div class="role-name-cell">
|
||||
<span class="role-avatar">{{ getAvatarText(scope.row.username) }}</span>
|
||||
<div>
|
||||
<strong>{{ scope.row.username }}</strong>
|
||||
<small>{{ getRoleText(scope.row.roles) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="email" label="邮箱" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="mobile" label="手机号" width="150" align="center" />
|
||||
<el-table-column label="角色权限" width="180" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ getRoleText(scope.row.roles) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_at" label="创建时间" width="180" align="center" />
|
||||
<el-table-column label="操作" width="140" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="openAssignDialog(scope.row)">分配角色</el-button>
|
||||
<!-- <el-button type="text" size="mini" class="danger-text">删除</el-button> -->
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="table-pagination">
|
||||
<span class="pagination-info">共 {{ total }} 条</span>
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next, jumper"
|
||||
:current-page.sync="currentPage"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
:visible.sync="addDialogVisible"
|
||||
append-to-body
|
||||
width="520px"
|
||||
custom-class="role-add-dialog"
|
||||
:close-on-click-modal="false"
|
||||
@close="resetAddForm"
|
||||
>
|
||||
<div slot="title" class="dialog-title">
|
||||
<div class="dialog-icon">
|
||||
<i class="el-icon-user-solid"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3>添加角色用户</h3>
|
||||
<p>创建角色账号,并配置对应角色权限。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form ref="addForm" :model="addForm" :rules="addRules" label-position="top" class="dialog-form">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model.trim="addForm.username" maxlength="30" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input v-model.trim="addForm.password" type="password" show-password maxlength="30" placeholder="请输入密码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model.trim="addForm.email" maxlength="60" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="mobile">
|
||||
<el-input v-model.trim="addForm.mobile" maxlength="11" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="addDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" class="submit-btn" @click="submitAddRole()">确认添加</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:visible.sync="assignDialogVisible"
|
||||
append-to-body
|
||||
width="500px"
|
||||
custom-class="role-add-dialog"
|
||||
:close-on-click-modal="false"
|
||||
@close="resetAssignForm"
|
||||
>
|
||||
<div slot="title" class="dialog-title">
|
||||
<div class="dialog-icon">
|
||||
<i class="el-icon-s-check"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3>分配角色</h3>
|
||||
<p>为 {{ currentAssignUser.username || '当前用户' }} 配置可用角色权限。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form ref="assignForm" :model="assignForm" :rules="assignRules" label-position="top" class="dialog-form">
|
||||
<el-form-item label="角色权限" prop="roleid">
|
||||
<el-select
|
||||
v-model="assignForm.roleid"
|
||||
multiple
|
||||
collapse-tags
|
||||
placeholder="请选择角色权限"
|
||||
class="full-select"
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in roleOptions"
|
||||
:key="item.id"
|
||||
:label="item.role"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="assignDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" class="submit-btn" @click="submitAssignRole">确认分配</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addRoleAPI, assignRoleAPI, getListAPI, getRoleAPI } from '@/api/roleManagement';
|
||||
|
||||
export default {
|
||||
name: 'RoleManagement',
|
||||
data() {
|
||||
return {
|
||||
queryForm: {
|
||||
username: '',
|
||||
mobile: ''
|
||||
},
|
||||
loading: false,
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
addDialogVisible: false,
|
||||
assignDialogVisible: false,
|
||||
currentAssignUser: {},
|
||||
addForm: this.createEmptyRole(),
|
||||
assignForm: {
|
||||
roleid: []
|
||||
},
|
||||
roleOptions: [],
|
||||
addRules: {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱', trigger: 'blur' }
|
||||
],
|
||||
mobile: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
]
|
||||
},
|
||||
assignRules: {
|
||||
roleid: [{ required: true, type: 'array', message: '请选择角色权限', trigger: 'change' }]
|
||||
},
|
||||
roles: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getRoleList()
|
||||
this.getUserList()
|
||||
},
|
||||
methods: {
|
||||
createEmptyRole() {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
email: '',
|
||||
mobile: ''
|
||||
}
|
||||
},
|
||||
openAddDialog() {
|
||||
if (!this.roleOptions.length) this.getRoleList()
|
||||
this.addDialogVisible = true
|
||||
},
|
||||
// 获取角色
|
||||
async getRoleList() {
|
||||
const orgType = sessionStorage.getItem('org_type') || sessionStorage.getItem('orgType') || ''
|
||||
try {
|
||||
const res = await getRoleAPI({ org_type: orgType })
|
||||
if (res && res.status === true) {
|
||||
this.roleOptions = this.normalizeRoleOptions(res.data)
|
||||
return
|
||||
}
|
||||
this.roleOptions = []
|
||||
} catch (error) {
|
||||
this.roleOptions = []
|
||||
this.$message.error('角色权限获取失败')
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1
|
||||
this.getUserList()
|
||||
},
|
||||
resetSearch() {
|
||||
this.queryForm = {
|
||||
username: '',
|
||||
mobile: ''
|
||||
}
|
||||
this.currentPage = 1
|
||||
this.getUserList()
|
||||
},
|
||||
handlePageChange(page) {
|
||||
this.currentPage = page
|
||||
this.getUserList()
|
||||
},
|
||||
getAvatarText(username) {
|
||||
return String(username || '-').slice(0, 1).toUpperCase()
|
||||
},
|
||||
getRoleText(roles) {
|
||||
return Array.isArray(roles) && roles.length
|
||||
? roles.map(item => item.name).join('、')
|
||||
: '-'
|
||||
},
|
||||
normalizeRoleOptions(list) {
|
||||
return Array.isArray(list)
|
||||
? list.map(item => ({
|
||||
...item,
|
||||
role: item.role || item.name || ''
|
||||
}))
|
||||
: []
|
||||
},
|
||||
mergeRoleOptions(roles = []) {
|
||||
const nextOptions = [...this.roleOptions]
|
||||
roles.forEach(role => {
|
||||
if (role && role.id && !nextOptions.some(item => item.id === role.id)) {
|
||||
nextOptions.push({
|
||||
id: role.id,
|
||||
role: role.name || role.role || role.id
|
||||
})
|
||||
}
|
||||
})
|
||||
this.roleOptions = nextOptions
|
||||
},
|
||||
getOrgId() {
|
||||
return sessionStorage.getItem('orgid') || ''
|
||||
},
|
||||
async getUserList() {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await getListAPI({
|
||||
orgid: this.getOrgId(),
|
||||
current_page: this.currentPage,
|
||||
page_size: this.pageSize,
|
||||
username: this.queryForm.username,
|
||||
mobile: this.queryForm.mobile
|
||||
})
|
||||
if (res && res.status === true && res.data) {
|
||||
this.roles = Array.isArray(res.data.rows) ? res.data.rows : []
|
||||
this.total = Number(res.data.total_count || 0)
|
||||
this.currentPage = Number(res.data.current_page || this.currentPage)
|
||||
this.pageSize = Number(res.data.page_size || this.pageSize)
|
||||
return
|
||||
}
|
||||
this.roles = []
|
||||
this.total = 0
|
||||
} catch (error) {
|
||||
this.roles = []
|
||||
this.total = 0
|
||||
this.$message.error('用户角色列表获取失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
resetAddForm() {
|
||||
this.addForm = this.createEmptyRole()
|
||||
this.$nextTick(() => {
|
||||
this.$refs.addForm && this.$refs.addForm.clearValidate()
|
||||
})
|
||||
},
|
||||
resetAssignForm() {
|
||||
this.currentAssignUser = {}
|
||||
this.assignForm = {
|
||||
roleid: []
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.$refs.assignForm && this.$refs.assignForm.clearValidate()
|
||||
})
|
||||
},
|
||||
submitAddRole() {
|
||||
this.$refs.addForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
await this.addUser()
|
||||
})
|
||||
},
|
||||
// 添加角色用户
|
||||
async addUser() {
|
||||
const orgType = sessionStorage.getItem('org_type') || sessionStorage.getItem('orgType') || ''
|
||||
const data = {
|
||||
username: this.addForm.username,
|
||||
password: this.addForm.password,
|
||||
email: this.addForm.email,
|
||||
mobile: this.addForm.mobile,
|
||||
orgid: this.getOrgId(),
|
||||
org_type: orgType
|
||||
}
|
||||
try {
|
||||
const res = await addRoleAPI(data)
|
||||
if (res && res.status === true) {
|
||||
const now = new Date()
|
||||
const createdAt = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
|
||||
this.roles.unshift({
|
||||
username: this.addForm.username,
|
||||
email: this.addForm.email,
|
||||
mobile: this.addForm.mobile,
|
||||
roles: [],
|
||||
create_at: createdAt
|
||||
})
|
||||
this.total += 1
|
||||
this.$message.success(res.msg || '角色用户添加成功')
|
||||
this.addDialogVisible = false
|
||||
this.getUserList()
|
||||
return
|
||||
}
|
||||
this.$message.error((res && res.msg) || '角色用户添加失败')
|
||||
} catch (error) {
|
||||
this.$message.error('角色用户添加失败')
|
||||
}
|
||||
},
|
||||
async openAssignDialog(row) {
|
||||
this.currentAssignUser = row || {}
|
||||
if (!this.roleOptions.length) await this.getRoleList()
|
||||
this.mergeRoleOptions(row.roles || [])
|
||||
this.assignForm.roleid = Array.isArray(row.roles) ? row.roles.map(item => item.id) : []
|
||||
this.assignDialogVisible = true
|
||||
},
|
||||
submitAssignRole() {
|
||||
this.$refs.assignForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
try {
|
||||
const res = await assignRoleAPI({
|
||||
userid: this.currentAssignUser.id,
|
||||
roleid: this.assignForm.roleid
|
||||
})
|
||||
if (res && res.status === true) {
|
||||
this.$message.success(res.msg || '角色分配成功')
|
||||
this.assignDialogVisible = false
|
||||
this.getUserList()
|
||||
return
|
||||
}
|
||||
this.$message.error((res && res.msg) || '角色分配失败')
|
||||
} catch (error) {
|
||||
this.$message.error('角色分配失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.role-management-page {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 24px;
|
||||
background: #f3f7ff;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 6px;
|
||||
color: #3b82f6;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.page-head h1 {
|
||||
margin: 0;
|
||||
color: #111827;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.page-head span {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.add-role-btn {
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.filter-card,
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
padding: 16px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-form ::v-deep .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-form ::v-deep .el-input__inner {
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.filter-form ::v-deep .el-button--small {
|
||||
height: 32px;
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
border-top: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.role-table ::v-deep th {
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.role-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.role-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #2563eb;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
background: #eff6ff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.role-name-cell strong {
|
||||
display: block;
|
||||
color: #1f2937;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.role-name-cell small {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.danger-text {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
::v-deep .role-add-dialog {
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .role-add-dialog .el-dialog__header {
|
||||
padding: 26px 30px 18px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
::v-deep .role-add-dialog .el-dialog__body {
|
||||
padding: 24px 30px 6px;
|
||||
}
|
||||
|
||||
::v-deep .role-add-dialog .el-dialog__footer {
|
||||
padding: 16px 30px 28px;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.dialog-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #2563eb;
|
||||
font-size: 20px;
|
||||
background: linear-gradient(135deg, #eff6ff, #dbeafe);
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.dialog-title h3 {
|
||||
margin: 0;
|
||||
color: #111827;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.dialog-title p {
|
||||
margin: 5px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dialog-form ::v-deep .el-form-item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.dialog-form ::v-deep .el-form-item__label {
|
||||
padding-bottom: 7px;
|
||||
color: #334155;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dialog-form ::v-deep .el-input__inner,
|
||||
.dialog-form ::v-deep .el-textarea__inner {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.full-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dialog-footer .el-button {
|
||||
min-width: 96px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: #2563eb;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user