This commit is contained in:
hrx 2026-07-22 11:34:07 +08:00
parent f9609403b5
commit 2faa82f4be
8 changed files with 910 additions and 130 deletions

View 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
})
}

View File

@ -2305,6 +2305,13 @@ export const asyncRoutes = [
path: "/superAdministrator", component: Layout, redirect: "/superAdministrator/index", meta: { path: "/superAdministrator", component: Layout, redirect: "/superAdministrator/index", meta: {
title: "超级管理员", icon: "el-icon-user-solid", noCache: true, fullPath: "/superAdministrator", title: "超级管理员", icon: "el-icon-user-solid", noCache: true, fullPath: "/superAdministrator",
}, children: [{ }, 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: { path: "addAdmin", component: () => import("@/views/superAdministrator/addAdmin"), name: "addAdmin", meta: {
title: "添加业主机构管理员", fullPath: "/superAdministrator/addAdmin", title: "添加业主机构管理员", fullPath: "/superAdministrator/addAdmin",
}, },

View File

@ -7,6 +7,7 @@ const MOBILE_UA_REGEXP = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Ope
const CUSTOMER_ROLE = '客户'; const CUSTOMER_ROLE = '客户';
const OPERATION_ROLE = '运营'; const OPERATION_ROLE = '运营';
const FINANCE_ROLE = '财务'; const FINANCE_ROLE = '财务';
const ADMINISTRATOR_ROLE = '管理员';
// 这个用户能看到订单管理里的特殊子菜单,比如历史订单和订单详情。 // 这个用户能看到订单管理里的特殊子菜单,比如历史订单和订单详情。
const SPECIAL_ORDER_USER = 'ZhipuHZ'; const SPECIAL_ORDER_USER = 'ZhipuHZ';
@ -23,6 +24,8 @@ const OPERATION_EXTRA_ROUTE_PATHS = ['/modelManagement', '/modelInfoConfig', '/o
// 财务角色需要额外补出来的菜单。 // 财务角色需要额外补出来的菜单。
const FINANCE_EXTRA_ROUTE_PATHS = ['/financialOverview']; const FINANCE_EXTRA_ROUTE_PATHS = ['/financialOverview'];
const ADMINISTRATOR_ROUTE_FULL_PATH = '/superAdministrator/roleManagement';
// 普通客户账号默认要补出来的基础菜单。 // 普通客户账号默认要补出来的基础菜单。
const BASE_USER_ROUTE_PATHS = ['/orderManagement', '/resourceManagement']; 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)); 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市集是公共菜单所有登录用户都要能看到。 // token市集是公共菜单所有登录用户都要能看到。
function addCommonRoutes(accessedRoutes, routes, deviceType = 'pc') { function addCommonRoutes(accessedRoutes, routes, deviceType = 'pc') {
const commonRoutes = getRoutesByPath(routes, COMMON_ROUTE_PATHS) const commonRoutes = getRoutesByPath(routes, COMMON_ROUTE_PATHS)
@ -495,6 +522,13 @@ const actions = {
console.log("用户类型:", userType, "orgType:", orgType, "设备类型:", deviceType); console.log("用户类型:", userType, "orgType:", orgType, "设备类型:", deviceType);
console.log("ACTION generateRoutes - auths:", auths); 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 过滤。 // 3. 先生成第一版菜单:超级管理员只拿超管菜单,普通用户按后端 auths 过滤。
let accessedRoutes = isSuperAdmin let accessedRoutes = isSuperAdmin
? getSuperAdminRoutes(deviceType) ? getSuperAdminRoutes(deviceType)
@ -502,6 +536,7 @@ const actions = {
// 4. token市集是公共入口所有登录用户都补上。 // 4. token市集是公共入口所有登录用户都补上。
accessedRoutes = addCommonRoutes(accessedRoutes, asyncRoutes, deviceType); accessedRoutes = addCommonRoutes(accessedRoutes, asyncRoutes, deviceType);
accessedRoutes = addAdministratorRoutes(accessedRoutes, asyncRoutes, userRoles, deviceType);
if (!isSuperAdmin) { if (!isSuperAdmin) {
// 5. 普通用户再补一些固定入口,比如订单、资源、客户专属菜单。 // 5. 普通用户再补一些固定入口,比如订单、资源、客户专属菜单。

View File

@ -382,6 +382,11 @@ export default {
}) })
}, },
redirectAfterLogin(res) { redirectAfterLogin(res) {
if (res.roles && res.roles.includes('管理员')) {
sessionStorage.removeItem('loginRedirectPath')
this.$router.push('/superAdministrator/roleManagement').catch(() => {})
return
}
const redirectPath = sessionStorage.getItem('loginRedirectPath') const redirectPath = sessionStorage.getItem('loginRedirectPath')
if (redirectPath && redirectPath.startsWith('/') && !redirectPath.includes('/login')) { if (redirectPath && redirectPath.startsWith('/') && !redirectPath.includes('/login')) {
sessionStorage.removeItem('loginRedirectPath') sessionStorage.removeItem('loginRedirectPath')

View File

@ -544,6 +544,7 @@ export default Vue.extend({
else if (role.includes('运维')) this.$router.push('/operationAndMaintenance/workOrderProcessing') 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('/sales/distributorManagement')
else if (role.includes('财务')) this.$router.push('/finance/supplierSettlementStatistics') 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') else if (role.includes('admin')) this.$router.push('/superAdministrator/addAdmin')
}, },
async logout() { async logout() {

View File

@ -243,7 +243,7 @@ export default {
this.stopObserveEditorFullscreen() this.stopObserveEditorFullscreen()
this.editorFullscreen = false this.editorFullscreen = false
}, },
methods: { methods: {
createEmptyForm() { createEmptyForm() {
// 使 // 使
return { return {

View File

@ -5,7 +5,7 @@
<i class="el-icon-arrow-left"></i> <i class="el-icon-arrow-left"></i>
返回 返回
</button> </button>
<span>API文档</span> <span>元境 API 文档</span>
</header> </header>
<main class="doc-container"> <main class="doc-container">
@ -19,40 +19,12 @@
></el-alert> ></el-alert>
<section class="doc-hero"> <section class="doc-hero">
<h1>{{ apiDoc.model_name }} API 文档</h1> <h1>元境 API 文档</h1>
<p>{{ heroDescription }}</p> <p>统一展示大模型视频图像音频任务查询等接口说明</p>
<!-- <div class="quick-tabs">
<span v-for="item in quickTabs" :key="item">{{ item }}</span>
</div> -->
</section> </section>
<section v-loading="loading" class="doc-section"> <section v-loading="loading" class="doc-section markdown-section">
<h2>1. 接口地址</h2> <div v-html="renderedMarkdown"></div>
<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> </section>
</main> </main>
@ -61,124 +33,137 @@
</template> </template>
<script> <script>
import { reqModelApiDocument } from '@/api/model/model'
export default { export default {
name: 'ApiDocument', name: 'ApiDocument',
data() { data() {
return { return {
loading: false, loading: false,
errorMessage: '', errorMessage: '',
apiDoc: { markdownText: '',
id: '', markdownUrl: 'https://token.opencomputing.cn/docs/api_doc_zh.md'
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())`
} }
}, },
computed: { computed: {
apiUrlText() { renderedMarkdown() {
const curlUrl = this.getUrlFromCurl(this.apiDoc.curl_code) return this.renderMarkdown(this.markdownText)
const url = this.apiDoc.api_url || curlUrl
return url ? `POST ${url}` : '接口地址以后端返回为准'
},
heroDescription() {
const modelName = this.apiDoc.model_name || '当前模型'
return `${modelName} 接口调用示例,包含 curl 与 Python 两种接入方式。`
} }
}, },
created() { created() {
this.fetchApiDocument() this.fetchMarkdownDocument()
},
watch: {
'$route.query.id'() {
this.fetchApiDocument()
}
}, },
methods: { methods: {
// id API async fetchMarkdownDocument() {
async fetchApiDocument() {
const id = this.$route.query.id || this.$route.query.model_id
if (!id) {
this.errorMessage = '缺少模型ID无法获取 API 文档'
return
}
this.loading = true this.loading = true
this.errorMessage = '' this.errorMessage = ''
try { try {
const res = await reqModelApiDocument({ id }) const response = await fetch(this.markdownUrl)
if (res && res.status && res.data) { if (!response.ok) {
this.apiDoc = { throw new Error(`HTTP ${response.status}`)
...this.apiDoc,
...res.data,
model_name: res.data.model_name || this.apiDoc.model_name
}
return
} }
this.errorMessage = (res && res.msg) || 'API 文档数据获取失败' this.markdownText = await response.text()
} catch (error) { } catch (error) {
console.error('[API文档] 获取模型 API 文档失败', error) console.error('[API文档] 获取 Markdown 文档失败', error)
this.errorMessage = 'API 文档数据获取失败,请稍后重试' this.errorMessage = 'API 文档加载失败,请稍后重试'
} finally { } finally {
this.loading = false this.loading = false
} }
}, },
getUrlFromCurl(curlCode) { escapeHtml(text = '') {
if (!curlCode) return '' return String(text)
const match = String(curlCode).match(/https?:\/\/[^\s\\]+/) .replace(/&/g, '&amp;')
return match ? match[0] : '' .replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
},
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() { goBack() {
this.$router.back() 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 { pre {
margin: 0; margin: 14px 0 24px;
padding: 16px; padding: 16px;
color: #e6edf3; color: #e6edf3;
overflow-x: auto; overflow-x: auto;

View File

@ -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>