main #143
@ -35,6 +35,8 @@ async def get_user_balance(ns={}):
|
||||
}
|
||||
orgid = await sor.R('organization', {'id': user[0]['orgid']})
|
||||
balance = await getCustomerBalance(sor, orgid[0]['id'])
|
||||
ns['balance'] = balance
|
||||
debug(ns)
|
||||
return {
|
||||
'status': 'ok',
|
||||
'balance': balance
|
||||
|
||||
94
b/cntoai/model_management_customer_search copy.dspy
Normal file
94
b/cntoai/model_management_customer_search copy.dspy
Normal file
@ -0,0 +1,94 @@
|
||||
def _escape(value):
|
||||
if value is None:
|
||||
return None
|
||||
return str(value).replace("'", "''")
|
||||
|
||||
# 客户侧可见字段(不含 listing_status、is_active 等运营字段)
|
||||
_CUSTOMER_MODEL_COLUMNS = """
|
||||
id, llmid, provider, model_name, display_name, model_logo, model_type,
|
||||
context_length, input_token_price, output_token_price,
|
||||
cache_hit_input_price, billing_method, billing_unit,
|
||||
capabilities, limitations, highlights, description, sort_order,is_active, experience
|
||||
"""
|
||||
|
||||
|
||||
def _customer_listed_conditions(ns):
|
||||
"""已上架且启用的模型;支持按厂商、模型类别筛选"""
|
||||
conditions = ["listing_status = 1", "is_active = 1"]
|
||||
if ns.get('provider'):
|
||||
conditions.append("provider = '%s'" % _escape(ns.get('provider')))
|
||||
if ns.get('model_type'):
|
||||
conditions.append("model_type = '%s'" % _escape(ns.get('model_type')))
|
||||
return ' AND '.join(conditions)
|
||||
|
||||
async def model_management_customer_search(ns={}):
|
||||
"""
|
||||
客户查看模型列表:仅已上架且启用的模型。
|
||||
|
||||
可选参数:
|
||||
provider (str) 厂商,精确匹配筛选
|
||||
model_type (str) 模型类别,精确匹配筛选
|
||||
current_page (int) 页码,默认 1
|
||||
page_size (int) 每页条数,默认 10
|
||||
|
||||
返回 data:
|
||||
provider_list 当前可见模型中的厂商列表(去重)
|
||||
model_type_list 当前可见模型中的模型类别列表(去重)
|
||||
filter_total 当前筛选条件下的模型数量
|
||||
model_list 模型列表
|
||||
page_size, current_page
|
||||
|
||||
调用示例见 model_management_customer_search.dspy
|
||||
"""
|
||||
page_size = int(ns.get('page_size', 1000))
|
||||
current_page = int(ns.get('current_page', 1))
|
||||
offset = (current_page - 1) * page_size
|
||||
where_clause = _customer_listed_conditions(ns)
|
||||
listed_base = "listing_status = 1 AND is_active = 1"
|
||||
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
try:
|
||||
provider_sql = """
|
||||
SELECT DISTINCT provider FROM model_management
|
||||
WHERE %s AND provider IS NOT NULL AND provider != ''
|
||||
ORDER BY provider;
|
||||
""" % listed_base
|
||||
model_type_sql = """
|
||||
SELECT DISTINCT model_type FROM model_management
|
||||
WHERE %s AND model_type IS NOT NULL AND model_type != ''
|
||||
ORDER BY model_type;
|
||||
""" % listed_base
|
||||
|
||||
count_sql = """
|
||||
SELECT COUNT(*) AS total_count FROM model_management WHERE %s;
|
||||
""" % where_clause
|
||||
find_sql = """
|
||||
SELECT %s FROM model_management
|
||||
WHERE %s
|
||||
ORDER BY sort_order ASC
|
||||
LIMIT %s OFFSET %s;
|
||||
""" % (_CUSTOMER_MODEL_COLUMNS, where_clause, page_size, offset)
|
||||
|
||||
provider_rows = await sor.sqlExe(provider_sql, {})
|
||||
model_type_rows = await sor.sqlExe(model_type_sql, {})
|
||||
filter_total = (await sor.sqlExe(count_sql, {}))[0]['total_count']
|
||||
model_list = await sor.sqlExe(find_sql, {})
|
||||
|
||||
return {
|
||||
'status': True,
|
||||
'msg': 'customer model search success',
|
||||
'data': {
|
||||
'provider_list': [r['provider'] for r in provider_rows],
|
||||
'model_type_list': [r['model_type'] for r in model_type_rows],
|
||||
'filter_total': filter_total,
|
||||
'page_size': page_size,
|
||||
'current_page': current_page,
|
||||
'model_list': model_list,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
return {'status': False, 'msg': 'customer model search failed, %s' % str(e)}
|
||||
|
||||
ret = await model_management_customer_search1(params_kw)
|
||||
return ret
|
||||
@ -3,22 +3,38 @@ def _escape(value):
|
||||
return None
|
||||
return str(value).replace("'", "''")
|
||||
|
||||
# 客户侧可见字段(不含 listing_status、is_active 等运营字段)
|
||||
# 客户侧返回字段结构保持与原 model_management 版本一致
|
||||
_CUSTOMER_MODEL_COLUMNS = """
|
||||
id, llmid, provider, model_name, display_name, model_logo, model_type,
|
||||
context_length, input_token_price, output_token_price,
|
||||
cache_hit_input_price, billing_method, billing_unit,
|
||||
capabilities, limitations, highlights, description, sort_order,is_active, experience
|
||||
id,
|
||||
id AS llmid,
|
||||
provider_name AS provider,
|
||||
model AS model_name,
|
||||
name AS display_name,
|
||||
provider_iconurl AS model_logo,
|
||||
catelogname AS model_type,
|
||||
NULL AS context_length,
|
||||
0 AS input_token_price,
|
||||
0 AS output_token_price,
|
||||
0 AS cache_hit_input_price,
|
||||
pricing_display AS billing_method,
|
||||
'' AS billing_unit,
|
||||
description AS capabilities,
|
||||
'' AS limitations,
|
||||
'' AS highlights,
|
||||
description,
|
||||
0 AS sort_order,
|
||||
1 AS is_active,
|
||||
0 AS experience
|
||||
"""
|
||||
|
||||
|
||||
def _customer_listed_conditions(ns):
|
||||
"""已上架且启用的模型;支持按厂商、模型类别筛选"""
|
||||
conditions = ["listing_status = 1", "is_active = 1"]
|
||||
conditions = ["status = 'published'"]
|
||||
if ns.get('provider'):
|
||||
conditions.append("provider = '%s'" % _escape(ns.get('provider')))
|
||||
conditions.append("provider_name = '%s'" % _escape(ns.get('provider')))
|
||||
if ns.get('model_type'):
|
||||
conditions.append("model_type = '%s'" % _escape(ns.get('model_type')))
|
||||
conditions.append("catelogname = '%s'" % _escape(ns.get('model_type')))
|
||||
return ' AND '.join(conditions)
|
||||
|
||||
async def model_management_customer_search(ns={}):
|
||||
@ -44,27 +60,27 @@ async def model_management_customer_search(ns={}):
|
||||
current_page = int(ns.get('current_page', 1))
|
||||
offset = (current_page - 1) * page_size
|
||||
where_clause = _customer_listed_conditions(ns)
|
||||
listed_base = "listing_status = 1 AND is_active = 1"
|
||||
listed_base = "status = 'published'"
|
||||
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
try:
|
||||
provider_sql = """
|
||||
SELECT DISTINCT provider FROM model_management
|
||||
WHERE %s AND provider IS NOT NULL AND provider != ''
|
||||
SELECT DISTINCT provider_name AS provider FROM llm
|
||||
WHERE %s AND provider_name IS NOT NULL AND provider_name != ''
|
||||
ORDER BY provider;
|
||||
""" % listed_base
|
||||
model_type_sql = """
|
||||
SELECT DISTINCT model_type FROM model_management
|
||||
WHERE %s AND model_type IS NOT NULL AND model_type != ''
|
||||
SELECT DISTINCT catelogname AS model_type FROM llm
|
||||
WHERE %s AND catelogname IS NOT NULL AND catelogname != ''
|
||||
ORDER BY model_type;
|
||||
""" % listed_base
|
||||
|
||||
count_sql = """
|
||||
SELECT COUNT(*) AS total_count FROM model_management WHERE %s;
|
||||
SELECT COUNT(*) AS total_count FROM llm WHERE %s;
|
||||
""" % where_clause
|
||||
find_sql = """
|
||||
SELECT %s FROM model_management
|
||||
SELECT %s FROM llm
|
||||
WHERE %s
|
||||
ORDER BY sort_order ASC
|
||||
LIMIT %s OFFSET %s;
|
||||
|
||||
@ -1,6 +1,183 @@
|
||||
async def sync_model_to_llm(ns={}):
|
||||
import aiohttp
|
||||
|
||||
async def get_sync_orgid(sor):
|
||||
if ns.get('orgid'):
|
||||
return ns.get('orgid')
|
||||
user_li = await sor.R('users', {'id': await get_user()})
|
||||
return user_li[0].get('orgid') if user_li else ''
|
||||
|
||||
def build_llm_data(item):
|
||||
return {
|
||||
'id': item.get('id') or uuid(),
|
||||
'name': item.get('name'),
|
||||
'model': item.get('model'),
|
||||
'description': item.get('description'),
|
||||
'iconid': item.get('iconid'),
|
||||
'upappid': item.get('upappid'),
|
||||
'providerid': item.get('providerid'),
|
||||
'ownerid': item.get('ownerid') or '0',
|
||||
'enabled_date': item.get('enabled_date'),
|
||||
'expired_date': item.get('expired_date'),
|
||||
'min_balance': item.get('min_balance'),
|
||||
'status': item.get('status') or 'published',
|
||||
'catelogname': item.get('catelogname'),
|
||||
'catelog_id': item.get('catelog_id') or item.get('catelogid'),
|
||||
'llmcatelogid': item.get('catelog_id') or item.get('catelogid'),
|
||||
'apiname': item.get('apiname'),
|
||||
'query_apiname': item.get('query_apiname'),
|
||||
'query_period': item.get('query_period'),
|
||||
'ppid': item.get('ppid'),
|
||||
'provider_name': item.get('provider_name'),
|
||||
'pricing_display': item.get('pricing_display'),
|
||||
'provider_iconurl': item.get('provider_iconurl')
|
||||
}
|
||||
|
||||
async def ensure_provider(sor, provider_orgcode, provider_name, sync_orgid, item):
|
||||
exist_org_li = await sor.R('organization', {'orgcode': provider_orgcode, 'del_flg': '0'})
|
||||
if exist_org_li:
|
||||
provider_orgid = exist_org_li[0].get('id')
|
||||
await sor.U('organization', {
|
||||
'id': provider_orgid,
|
||||
'orgname': provider_name,
|
||||
'orgcode': provider_orgcode,
|
||||
'parentid': sync_orgid,
|
||||
'org_type': '4'
|
||||
})
|
||||
else:
|
||||
provider_orgid = uuid()
|
||||
await sor.C('organization', {
|
||||
'id': provider_orgid,
|
||||
'orgname': provider_name,
|
||||
'contactor': '连战',
|
||||
'contactor_phone': '13800138000',
|
||||
'address': '北京市海淀区',
|
||||
'main_business': provider_name,
|
||||
'orgcode': provider_orgcode,
|
||||
'emailaddress': 'lianzhan@cntoai.com',
|
||||
'org_type': '4',
|
||||
'parentid': sync_orgid
|
||||
})
|
||||
await openProviderAccounts(sor, sync_orgid, provider_orgid)
|
||||
|
||||
provider_li = await sor.R('provider', {'orgid': provider_orgid, 'del_flg': '0'})
|
||||
provider_data = {
|
||||
'name': provider_name,
|
||||
'description': provider_name,
|
||||
'appid': item.get('upappid') or '',
|
||||
'providerkey': provider_orgcode,
|
||||
'state': '1',
|
||||
'start_date': '2026-07-01',
|
||||
'end_date': '9999-12-31',
|
||||
'settle_mode': '3',
|
||||
'settle_flowid': '0',
|
||||
'settle_datep': 'M00',
|
||||
'discount_mode': 1,
|
||||
'orgid': provider_orgid
|
||||
}
|
||||
if provider_li:
|
||||
provider_id = provider_li[0].get('id')
|
||||
provider_data['id'] = provider_id
|
||||
await sor.U('provider', provider_data)
|
||||
else:
|
||||
provider_id = uuid()
|
||||
provider_data['id'] = provider_id
|
||||
await sor.C('provider', provider_data)
|
||||
|
||||
protocol_li = await sor.R('saleprotocol', {'offer_orgid': provider_orgid, 'bid_orgid': sync_orgid, 'del_flg': '0'})
|
||||
if protocol_li:
|
||||
protocolid = protocol_li[0].get('id')
|
||||
await sor.U('saleprotocol', {
|
||||
'id': protocolid,
|
||||
'offer_orgid': provider_orgid,
|
||||
'bid_orgid': sync_orgid,
|
||||
'salemode': '0',
|
||||
'settle_mode': '3',
|
||||
'settle_dp': 'M00',
|
||||
'start_date': '2026-07-01',
|
||||
'end_date': '9999-12-31'
|
||||
})
|
||||
else:
|
||||
protocolid = uuid()
|
||||
await sor.C('saleprotocol', {
|
||||
'id': protocolid,
|
||||
'offer_orgid': provider_orgid,
|
||||
'bid_orgid': sync_orgid,
|
||||
'salemode': '0',
|
||||
'settle_mode': '3',
|
||||
'settle_dp': 'M00',
|
||||
'start_date': '2026-07-01',
|
||||
'end_date': '9999-12-31'
|
||||
})
|
||||
|
||||
return {
|
||||
'provider_orgid': provider_orgid,
|
||||
'provider_id': provider_id,
|
||||
'protocolid': protocolid
|
||||
}
|
||||
|
||||
async def sync_product(sor, item, category_list, provider_orgid, protocolid, sync_orgid):
|
||||
product_data = {
|
||||
'providerid': provider_orgid,
|
||||
'providerpid': item.get('id'),
|
||||
'name': item.get('name'),
|
||||
# 'description': item.get('description') or '',
|
||||
'ptype': '21',
|
||||
'state': '1',
|
||||
'salemode': '0',
|
||||
'product_code': item.get('model'),
|
||||
'reseller_orgid': sync_orgid,
|
||||
'classify': 'A',
|
||||
'label': item.get('catelogname') or category_list.get('catelogname'),
|
||||
# 'productgroup': item.get('catelog_id') or item.get('catelogid') or category_list.get('catelogid'),
|
||||
'product_area': '北京',
|
||||
'effect_date': '2026-07-01',
|
||||
'expire_date': '9999-12-31'
|
||||
}
|
||||
product_li = await sor.R('product', {'providerid': provider_orgid, 'product_code': item.get('model'), 'del_flg': '0'})
|
||||
if product_li:
|
||||
product_id = product_li[0].get('id')
|
||||
product_data['id'] = product_id
|
||||
await sor.U('product', product_data)
|
||||
product_action = 'update'
|
||||
else:
|
||||
product_id = uuid()
|
||||
product_data['id'] = product_id
|
||||
await sor.C('product', product_data)
|
||||
product_action = 'insert'
|
||||
|
||||
salemode_data = {
|
||||
'protocolid': protocolid,
|
||||
'providerid': provider_orgid,
|
||||
'productid': product_id,
|
||||
'discount': 1,
|
||||
'unit': 'hour'
|
||||
}
|
||||
product_salemode_li = await sor.R('product_salemode', {
|
||||
'protocolid': protocolid,
|
||||
'providerid': provider_orgid,
|
||||
'productid': product_id,
|
||||
'del_flg': '0'
|
||||
})
|
||||
if product_salemode_li:
|
||||
salemode_data['id'] = product_salemode_li[0].get('id')
|
||||
await sor.U('product_salemode', salemode_data)
|
||||
else:
|
||||
salemode_data['id'] = uuid()
|
||||
await sor.C('product_salemode', salemode_data)
|
||||
|
||||
return product_action
|
||||
|
||||
async def upsert_llm(sor, item):
|
||||
llm_data = build_llm_data(item)
|
||||
exist_llm = await sor.R('llm', {'model': item.get('model')})
|
||||
if exist_llm:
|
||||
llm_data['id'] = exist_llm[0].get('id')
|
||||
await sor.U('llm', llm_data)
|
||||
return 'update'
|
||||
await sor.C('llm', llm_data)
|
||||
return 'insert'
|
||||
|
||||
# 从数据库读取domain和Bearer token
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
@ -13,6 +190,13 @@ async def sync_model_to_llm(ns={}):
|
||||
}
|
||||
domain = domain_li[0]['pvalue']
|
||||
bearer_token = user_key[0]['pvalue']
|
||||
# sync_orgid = await get_sync_orgid(sor)
|
||||
sync_orgid = "mIWUHBeeDM8mwAFPIQ8pS" # 固定写
|
||||
if not sync_orgid:
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '未找到当前用户所属机构'
|
||||
}
|
||||
|
||||
url = f"{domain}/llmage/list_llms"
|
||||
header = {
|
||||
@ -32,38 +216,93 @@ async def sync_model_to_llm(ns={}):
|
||||
# 插入数据库
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
new_llms_count = 0
|
||||
new_llms_list = []
|
||||
provider_groups = {}
|
||||
current_model_set = set()
|
||||
for category_list in result:
|
||||
for item in category_list.get('llms', []):
|
||||
# 查找数据库中是否已经存在,不存在就插入
|
||||
exist_llm = await sor.R('llm', {'model': item.get('model')})
|
||||
if exist_llm:
|
||||
provider_orgcode = item.get('providerid')
|
||||
model = item.get('model')
|
||||
if not provider_orgcode or not model:
|
||||
continue
|
||||
new_llms = {
|
||||
'id': item.get('id'),
|
||||
'name': item.get('name'),
|
||||
'model': item.get('model'),
|
||||
'description': item.get('description'),
|
||||
'llmcatelogid': item.get('catelog_id') or item.get('catelogid'),
|
||||
'iconid': item.get('iconid'),
|
||||
'upappid': item.get('upappid'),
|
||||
'apiname': item.get('apiname'),
|
||||
'providerid': item.get('providerid'),
|
||||
'ownerid': item.get('ownerid') or '0',
|
||||
'enabled_date': item.get('enabled_date'),
|
||||
'expired_date': item.get('expired_date'),
|
||||
'query_apiname': item.get('query_apiname') or '',
|
||||
'query_period': item.get('query_period'),
|
||||
'ppid': item.get('ppid'),
|
||||
}
|
||||
new_llms_count += 1
|
||||
new_llms_list.append(new_llms.get('model'))
|
||||
await sor.C('llm', new_llms)
|
||||
item['catelogname'] = item.get('catelogname') or category_list.get('catelogname')
|
||||
item['catelog_id'] = item.get('catelog_id') or item.get('catelogid') or category_list.get('catelogid')
|
||||
current_model_set.add(model)
|
||||
if provider_orgcode not in provider_groups:
|
||||
provider_groups[provider_orgcode] = {
|
||||
'provider_name': item.get('provider_name') or provider_orgcode or '模型供应商',
|
||||
'items': []
|
||||
}
|
||||
elif item.get('provider_name'):
|
||||
provider_groups[provider_orgcode]['provider_name'] = item.get('provider_name')
|
||||
provider_groups[provider_orgcode]['items'].append({
|
||||
'item': item,
|
||||
'category': category_list
|
||||
})
|
||||
|
||||
provider_count = 0
|
||||
product_insert_count = 0
|
||||
product_update_count = 0
|
||||
llm_insert_count = 0
|
||||
llm_update_count = 0
|
||||
synced_llms_list = []
|
||||
|
||||
for provider_orgcode, provider_group in provider_groups.items():
|
||||
provider_name = provider_group.get('provider_name') or provider_orgcode or '模型供应商'
|
||||
provider_data = await ensure_provider(
|
||||
sor,
|
||||
provider_orgcode,
|
||||
provider_name,
|
||||
sync_orgid,
|
||||
provider_group['items'][0]['item']
|
||||
)
|
||||
provider_count += 1
|
||||
|
||||
for model_info in provider_group['items']:
|
||||
item = model_info['item']
|
||||
category_list = model_info['category']
|
||||
product_action = await sync_product(
|
||||
sor,
|
||||
item,
|
||||
category_list,
|
||||
provider_data['provider_orgid'],
|
||||
provider_data['protocolid'],
|
||||
sync_orgid
|
||||
)
|
||||
if product_action == 'insert':
|
||||
product_insert_count += 1
|
||||
else:
|
||||
product_update_count += 1
|
||||
|
||||
llm_action = await upsert_llm(sor, item)
|
||||
if llm_action == 'insert':
|
||||
llm_insert_count += 1
|
||||
else:
|
||||
llm_update_count += 1
|
||||
synced_llms_list.append(item.get('model'))
|
||||
|
||||
llm_unpublished_count = 0
|
||||
exist_llm_li = await sor.R('llm', {})
|
||||
for exist_llm in exist_llm_li:
|
||||
exist_model = exist_llm.get('model')
|
||||
if exist_model and exist_model not in current_model_set and exist_llm.get('status') != 'unpublished':
|
||||
await sor.U('llm', {
|
||||
'id': exist_llm.get('id'),
|
||||
'status': 'unpublished'
|
||||
})
|
||||
llm_unpublished_count += 1
|
||||
|
||||
return {
|
||||
'status': True,
|
||||
'msg': f"sync_llm_list同步模型成功,共插入{new_llms_count}个模型,模型列表: {new_llms_list}"
|
||||
'msg': 'sync_llm_list同步模型成功',
|
||||
'data': {
|
||||
'provider_count': provider_count,
|
||||
'product_insert_count': product_insert_count,
|
||||
'product_update_count': product_update_count,
|
||||
'llm_insert_count': llm_insert_count,
|
||||
'llm_update_count': llm_update_count,
|
||||
'llm_unpublished_count': llm_unpublished_count,
|
||||
'model_list': synced_llms_list
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
|
||||
145
b/llm.sql
Normal file
145
b/llm.sql
Normal file
@ -0,0 +1,145 @@
|
||||
# 创建表格命令
|
||||
CREATE TABLE llm (
|
||||
id VARCHAR(64) NOT NULL COMMENT '唯一标识',
|
||||
name VARCHAR(50) NOT NULL COMMENT '模型名称',
|
||||
model VARCHAR(50) NOT NULL COMMENT '模型代号',
|
||||
description TEXT COMMENT '模型描述',
|
||||
iconid VARCHAR(50) COMMENT '图标标识',
|
||||
upappid VARCHAR(50) COMMENT '上层应用ID',
|
||||
providerid VARCHAR(64) COMMENT '供应商ID',
|
||||
ownerid VARCHAR(20) DEFAULT '0' COMMENT '所有者ID',
|
||||
enabled_date DATE COMMENT '启用日期',
|
||||
expired_date DATE COMMENT '过期日期',
|
||||
min_balance INT DEFAULT 0 COMMENT '最低余额要求(元)',
|
||||
status VARCHAR(20) COMMENT '状态(如published)',
|
||||
catelogname VARCHAR(50) COMMENT '目录名称(如文生视频)',
|
||||
catelog_id VARCHAR(20) COMMENT '目录ID',
|
||||
apiname VARCHAR(50) COMMENT 'API名称',
|
||||
query_apiname VARCHAR(50) COMMENT '查询API名称',
|
||||
query_period INT COMMENT '查询周期(秒)',
|
||||
ppid VARCHAR(64) COMMENT '定价计划ID',
|
||||
provider_name VARCHAR(100) COMMENT '供应商名称',
|
||||
pricing_display TEXT COMMENT '定价显示文本',
|
||||
provider_iconurl VARCHAR(255) COMMENT '供应商图标URL',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='LLM模型信息表';
|
||||
|
||||
|
||||
# 厂商返回的部分数据示例
|
||||
[
|
||||
{
|
||||
"catelogid": "2i2v",
|
||||
"catelogname": "首尾帧生视频",
|
||||
"llms": [
|
||||
{
|
||||
"id": "r0ill7L0s6PtBS8mkmg-7",
|
||||
"name": "viduq3-pro",
|
||||
"model": "viduq3-pro",
|
||||
"description": "vidu文生视频,viduq3-pro模型",
|
||||
"iconid": "vidu",
|
||||
"upappid": "vidu",
|
||||
"providerid": "Bu3Z6OXANehbkD6OK5NJb",
|
||||
"ownerid": "0",
|
||||
"enabled_date": "2026-01-01",
|
||||
"expired_date": "9999-12-31",
|
||||
"min_balance": 10,
|
||||
"status": "published",
|
||||
"catelogname": "首尾帧生视频",
|
||||
"catelog_id": "2i2v",
|
||||
"apiname": "2i2v",
|
||||
"query_apiname": "taskStatus",
|
||||
"query_period": 30,
|
||||
"ppid": "sF2gcl7UeANKtnZv8hfvL",
|
||||
"provider_name": "北京生数科技有限公司",
|
||||
"pricing_display": " - duration: 0.56 元/秒 ",
|
||||
"provider_iconurl": "https://token.opencomputing.cn/appbase/show_icon.dspy?id=vidu"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"catelogid": "asr",
|
||||
"catelogname": "语音识别",
|
||||
"llms": [
|
||||
{
|
||||
"id": "9glv3S-JSzxholxAblU14",
|
||||
"name": "qwen3-asr-flash",
|
||||
"model": "qwen3-asr-flash",
|
||||
"description": "通义千问系列的录音文件识",
|
||||
"iconid": "qwen",
|
||||
"upappid": "tongyi-wan",
|
||||
"providerid": "6fadgewjraOyvxC_EkHou",
|
||||
"ownerid": "0",
|
||||
"enabled_date": "2025-12-01",
|
||||
"expired_date": "9999-12-31",
|
||||
"min_balance": 10,
|
||||
"status": "published",
|
||||
"catelogname": "语音识别",
|
||||
"catelog_id": "asr",
|
||||
"apiname": "short_asr",
|
||||
"query_apiname": null,
|
||||
"query_period": null,
|
||||
"ppid": "16pwEsQeHiOrxC0O6A2j6",
|
||||
"provider_name": "阿里云",
|
||||
"provider_iconurl": "https://token.opencomputing.cn/appbase/show_icon.dspy?id=qwen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"catelogid": "digital_human",
|
||||
"catelogname": "数字人",
|
||||
"llms": [
|
||||
{
|
||||
"id": "KOsAo6YjTdU0KG7_sKUJF",
|
||||
"name": "wan2.2-s2v",
|
||||
"model": "wan2.2-s2v",
|
||||
"description": "模型:wan2.2-s2v,输入音频和照片,用照片中的人生成数字人",
|
||||
"iconid": "qwen",
|
||||
"upappid": "tongyi-wan",
|
||||
"providerid": "6fadgewjraOyvxC_EkHou",
|
||||
"ownerid": "0",
|
||||
"enabled_date": "2025-12-19",
|
||||
"expired_date": "9999-12-31",
|
||||
"min_balance": 10,
|
||||
"status": "published",
|
||||
"catelogname": "数字人",
|
||||
"catelog_id": "digital_human",
|
||||
"apiname": "avator",
|
||||
"query_apiname": "avatorstatus",
|
||||
"query_period": 10,
|
||||
"ppid": "0B6aldoAej1PpZ4ydtrEZ",
|
||||
"provider_name": "阿里云",
|
||||
"pricing_display": " - duration: 0.5 元/秒 [分辨率=480P]\n - duration: 0.9 元/秒 [分辨率=720P]",
|
||||
"provider_iconurl": "https://token.opencomputing.cn/appbase/show_icon.dspy?id=qwen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"catelogid": "i2v",
|
||||
"catelogname": "图生视频",
|
||||
"llms": [
|
||||
{
|
||||
"id": "7y7kfYM0Xasizyi37WvAZ",
|
||||
"name": "happyhorse-1.0-i2v",
|
||||
"model": "happyhorse-1.0-i2v",
|
||||
"description": "HappyHorse图生视频模型,以首帧图片为基础,支持通过文本描述进行引导,生成物理真实、运动流畅的视频。",
|
||||
"iconid": "qwen",
|
||||
"upappid": "tongyi-wan",
|
||||
"providerid": "6fadgewjraOyvxC_EkHou",
|
||||
"ownerid": "0",
|
||||
"enabled_date": "2026-04-28",
|
||||
"expired_date": "9999-12-31",
|
||||
"min_balance": 10,
|
||||
"status": "published",
|
||||
"catelogname": "图生视频",
|
||||
"catelog_id": "i2v",
|
||||
"apiname": "i2v-happyhorse",
|
||||
"query_apiname": "t2vstatus",
|
||||
"query_period": 10,
|
||||
"ppid": "knEwDP35DEz5oMAXyvwN3",
|
||||
"provider_name": "阿里云",
|
||||
"pricing_display": " - duration: 0.9 元/秒 [SR=720]\n - duration: 1.6 元/秒 [SR=1080]",
|
||||
"provider_iconurl": "https://token.opencomputing.cn/appbase/show_icon.dspy?id=qwen"
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -82,14 +82,6 @@ async def providerAdd(ns={}):
|
||||
'status': False,
|
||||
'msg': 'provider add failed, orgcode is empty'
|
||||
}
|
||||
# filter by orgcode
|
||||
res = await sor.R('organization', {})
|
||||
orgcodes = [orgc.get('orgcode') for orgc in res]
|
||||
if org_code in orgcodes:
|
||||
return {
|
||||
'status': False,
|
||||
'msg': 'provider add failed, orgcode has already exist'
|
||||
}
|
||||
if not ns.get('orgid'):
|
||||
orgid_li = await sor.R('users', {'id': await get_user()})
|
||||
orgid = orgid_li[0].get('orgid') if orgid_li else ''
|
||||
@ -101,8 +93,33 @@ async def providerAdd(ns={}):
|
||||
else:
|
||||
orgid = ns.get('orgid')
|
||||
|
||||
# filter by orgcode; existing provider returns ids for idempotent callers
|
||||
exist_org_li = await sor.R('organization', {'orgcode': org_code, 'del_flg': '0'})
|
||||
if len(exist_org_li) >= 1:
|
||||
exist_org = exist_org_li[0]
|
||||
exist_provider_li = await sor.R('provider', {'orgid': exist_org.get('id'), 'del_flg': '0'})
|
||||
if not exist_provider_li:
|
||||
return {
|
||||
'status': False,
|
||||
'msg': 'provider add failed, orgcode has already exist but provider not found'
|
||||
}
|
||||
exist_protocol_li = await sor.R('saleprotocol', {'offer_orgid': exist_org.get('id'), 'bid_orgid': orgid, 'del_flg': '0'})
|
||||
if not exist_protocol_li:
|
||||
exist_protocol_li = await sor.R('saleprotocol', {'offer_orgid': exist_org.get('id'), 'del_flg': '0'})
|
||||
return {
|
||||
'status': True,
|
||||
'msg': 'provider already exist',
|
||||
'data': {
|
||||
'orgid': exist_org.get('id'),
|
||||
'provider_orgid': exist_org.get('id'),
|
||||
'provider_id': exist_provider_li[0].get('id'),
|
||||
'protocolid': exist_protocol_li[0].get('id') if exist_protocol_li else None
|
||||
}
|
||||
}
|
||||
|
||||
# 供应商parentid 和 运营机构id相同
|
||||
ns_org['parentid'] = orgid
|
||||
ns_sale_mode['bid_orgid'] = orgid
|
||||
|
||||
if not ns_provider.get('start_date'):
|
||||
ns_provider['start_date'] = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
@ -115,7 +132,13 @@ async def providerAdd(ns={}):
|
||||
await openProviderAccounts(sor, orgid, ns_org['id'])
|
||||
return {
|
||||
"status": True,
|
||||
"msg": "provider add success"
|
||||
"msg": "provider add success",
|
||||
"data": {
|
||||
"orgid": ns_org['id'],
|
||||
"provider_orgid": ns_org['id'],
|
||||
"provider_id": ns_provider['id'],
|
||||
"protocolid": ns_sale_mode['id']
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@ -27,15 +27,16 @@ async def reseller_add_user(ns):
|
||||
try:
|
||||
ns['id'] = uuid()
|
||||
ns['password'] = password_encode(ns['password'])
|
||||
tenant_orgid = await get_reseller_user_tenant_orgid(sor, ns.get('orgid'))
|
||||
# tenant_orgid = await get_reseller_user_tenant_orgid(sor, ns.get('orgid'))
|
||||
tenant_orgid = ns.get('orgid')
|
||||
if not tenant_orgid:
|
||||
return {'status': False, 'msg': '未找到用户所属租户'}
|
||||
user_sql = """select id from users where username = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns['username'], tenant_orgid)
|
||||
user = await sor.sqlExe(user_sql, {})
|
||||
if len(user) >= 1:
|
||||
return {'status': False, 'msg': '用户名重复'}
|
||||
rolereacs = await sor.R('role', {'role': '管理员', 'del_flg': '0', 'org_type': '1'})
|
||||
ns['user_reseller'] = '1'
|
||||
rolereacs = await sor.R('role', {'role': '管理员', 'del_flg': '0', 'org_type': ns.get('org_type')})
|
||||
ns['user_reseller'] = ns.get('org_type')
|
||||
ns['tenant_orgid'] = tenant_orgid
|
||||
await sor.C('users', ns)
|
||||
await sor.C('userrole', {'userid': ns['id'], 'roleid': rolereacs[0]['id'], 'id': uuid()})
|
||||
|
||||
55
b/user/add_user_role.dspy
Normal file
55
b/user/add_user_role.dspy
Normal file
@ -0,0 +1,55 @@
|
||||
import json
|
||||
|
||||
|
||||
def _parse_roleids(roleid):
|
||||
if roleid is None:
|
||||
return []
|
||||
if isinstance(roleid, list):
|
||||
return [i for i in roleid if i]
|
||||
if isinstance(roleid, str):
|
||||
roleid = roleid.strip()
|
||||
if not roleid:
|
||||
return []
|
||||
try:
|
||||
roleids = json.loads(roleid)
|
||||
except Exception:
|
||||
roleids = [roleid]
|
||||
if isinstance(roleids, list):
|
||||
return [i for i in roleids if i]
|
||||
return [roleids] if roleids else []
|
||||
return [roleid]
|
||||
|
||||
|
||||
async def add_user_role(ns):
|
||||
"""
|
||||
用户分配角色
|
||||
:param userid: 用户id
|
||||
:param roleid: 角色id列表
|
||||
:return:
|
||||
"""
|
||||
if not ns or not ns.get('userid') or 'roleid' not in ns:
|
||||
return {'status': False, 'msg': '参数不正确'}
|
||||
|
||||
userid = ns.get('userid')
|
||||
roleids = _parse_roleids(ns.get('roleid'))
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
userroles = await sor.R('userrole', {'userid': userid})
|
||||
for userrole in userroles:
|
||||
if userrole.get('roleid') not in roleids:
|
||||
await sor.U('userrole', {'del_flg': '1', 'id': userrole['id']})
|
||||
|
||||
for roleid in roleids:
|
||||
roles = {'userid': userid, 'roleid': roleid}
|
||||
reacs = await sor.R('userrole', roles)
|
||||
if len(reacs) >= 1:
|
||||
await sor.U('userrole', {'id': reacs[0]['id'], 'del_flg': '0'})
|
||||
continue
|
||||
await sor.C('userrole', {'id': uuid(), 'userid': userid, 'roleid': roleid})
|
||||
return {'status': True, 'msg': '添加成功'}
|
||||
|
||||
return {'status': False, 'msg': '添加失败'}
|
||||
|
||||
|
||||
ret = await add_user_role(params_kw)
|
||||
return ret
|
||||
90
b/user/get_user_and_role.dspy
Normal file
90
b/user/get_user_and_role.dspy
Normal file
@ -0,0 +1,90 @@
|
||||
def _parse_page(ns, default_page=1, default_size=20, max_size=200):
|
||||
try:
|
||||
current_page = int(ns.get('current_page') or ns.get('page') or default_page)
|
||||
except (TypeError, ValueError):
|
||||
current_page = default_page
|
||||
try:
|
||||
page_size = int(ns.get('page_size') or default_size)
|
||||
except (TypeError, ValueError):
|
||||
page_size = default_size
|
||||
current_page = max(1, current_page)
|
||||
page_size = max(1, min(page_size, max_size))
|
||||
return current_page, page_size, (current_page - 1) * page_size
|
||||
|
||||
|
||||
async def get_user_and_role(ns={}):
|
||||
"""获取机构下所有用户及用户对应的所有角色"""
|
||||
orgid = ns.get('orgid')
|
||||
if not orgid:
|
||||
return {'status': False, 'msg': '缺少 orgid'}
|
||||
|
||||
current_page, page_size, offset = _parse_page(ns)
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
try:
|
||||
where_sql = """
|
||||
u.del_flg='0'
|
||||
AND (
|
||||
u.orgid=${orgid}$
|
||||
OR o.parentid=${orgid}$
|
||||
)
|
||||
"""
|
||||
if ns.get('username'):
|
||||
where_sql += " AND u.username LIKE '%%%%%s%%%%'" % ns.get('username')
|
||||
if ns.get('mobile'):
|
||||
where_sql += " AND u.mobile LIKE '%%%%%s%%%%'" % ns.get('mobile')
|
||||
count_rows = await sor.sqlExe(
|
||||
"""
|
||||
SELECT COUNT(*) AS total_count
|
||||
FROM users u
|
||||
LEFT JOIN organization o ON u.orgid = o.id AND o.del_flg='0'
|
||||
WHERE %s
|
||||
""" % where_sql,
|
||||
{'orgid': orgid},
|
||||
)
|
||||
total_count = count_rows[0]['total_count'] if count_rows else 0
|
||||
sql = """
|
||||
SELECT u.*
|
||||
FROM users u
|
||||
LEFT JOIN organization o ON u.orgid = o.id AND o.del_flg='0'
|
||||
LEFT JOIN (
|
||||
SELECT userid, MAX(create_at) AS userrole_create_at
|
||||
FROM userrole
|
||||
WHERE del_flg='0'
|
||||
GROUP BY userid
|
||||
) ur_sort ON u.id = ur_sort.userid
|
||||
WHERE %s
|
||||
ORDER BY ur_sort.userrole_create_at DESC, u.create_at DESC
|
||||
LIMIT %d OFFSET %d
|
||||
""" % (where_sql, page_size, offset)
|
||||
users = await sor.sqlExe(sql, {'orgid': orgid})
|
||||
for user in users:
|
||||
user['roles'] = []
|
||||
userroles = await sor.R('userrole', {'userid': user['id']})
|
||||
userroles = sorted(userroles, key=lambda item: item.get('create_at') or '', reverse=True)
|
||||
for userrole in userroles:
|
||||
if userrole.get('del_flg') == '1' or not userrole.get('roleid'):
|
||||
continue
|
||||
roles = await sor.R('role', {'id': userrole['roleid']})
|
||||
if len(roles) >= 1 and roles[0].get('del_flg') != '1':
|
||||
user['roles'].append({
|
||||
'id': roles[0].get('id'),
|
||||
'name': roles[0].get('role'),
|
||||
})
|
||||
|
||||
return {
|
||||
'status': True,
|
||||
'msg': 'ok',
|
||||
'data': {
|
||||
'total_count': total_count,
|
||||
'current_page': current_page,
|
||||
'page_size': page_size,
|
||||
'rows': users,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
return {'status': False, 'msg': '获取用户角色失败, %s' % str(e)}
|
||||
|
||||
|
||||
ret = await get_user_and_role(params_kw)
|
||||
return ret
|
||||
@ -4,6 +4,10 @@
|
||||
<router-view/>
|
||||
<FloatingBox
|
||||
v-if="!$route.path.includes('kyyForm')&&!$route.path.includes('floatingBox')&&!$route.path.includes('screen')&&!$route.path.includes('wxPage')&&!$route.path.includes('wxDetailPage')"/>
|
||||
<LoginDialog
|
||||
:visible.sync="loginDialogVisible"
|
||||
@success="handleGlobalLoginSuccess"
|
||||
/>
|
||||
<div>
|
||||
<!-- 弹出通话提示-->
|
||||
<div v-if="isShowVideo" class="is-accepted" style="padding: 5px;border-radius: 6px">
|
||||
@ -38,6 +42,7 @@
|
||||
<script>
|
||||
|
||||
import FloatingBox from './components/FloatingBox/FloatingBox.vue'; // 导入悬浮框组件
|
||||
import LoginDialog from '@/views/LoginDialog/LoginDialog.vue'
|
||||
import '@/assets/elementStyle.css'
|
||||
import Hammer from 'hammerjs';
|
||||
import {mapState} from "vuex";
|
||||
@ -45,6 +50,7 @@ export default {
|
||||
name: "App",
|
||||
components: {
|
||||
FloatingBox, // 注册悬浮框组件
|
||||
LoginDialog,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -52,7 +58,8 @@ export default {
|
||||
socket: null,
|
||||
userId: sessionStorage.getItem('userId'),
|
||||
vedioObj: {},
|
||||
customerName: ''
|
||||
customerName: '',
|
||||
loginDialogVisible: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -66,6 +73,10 @@ export default {
|
||||
}
|
||||
this.setRemUnit()
|
||||
this.initHammer();
|
||||
window.addEventListener('kboss-open-login-dialog', this.openGlobalLoginDialog)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('kboss-open-login-dialog', this.openGlobalLoginDialog)
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
@ -75,6 +86,16 @@ export default {
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
openGlobalLoginDialog() {
|
||||
if (this.$route && this.$route.fullPath && !this.$route.fullPath.includes('/login')) {
|
||||
sessionStorage.setItem('loginRedirectPath', this.$route.fullPath)
|
||||
}
|
||||
this.loginDialogVisible = true
|
||||
},
|
||||
handleGlobalLoginSuccess() {
|
||||
this.loginDialogVisible = false
|
||||
this.userId = sessionStorage.getItem('userId')
|
||||
},
|
||||
playAudio() {
|
||||
console.log("开始按钮点击了")
|
||||
this.audioElement.play();
|
||||
|
||||
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. 普通用户再补一些固定入口,比如订单、资源、客户专属菜单。
|
||||
|
||||
@ -165,16 +165,8 @@ service.interceptors.response.use(
|
||||
isRedirectingToLogin = true
|
||||
saveLoginRedirectPath()
|
||||
showErrorMessage('登录已过期,请重新登录')
|
||||
const currentPath = (router.currentRoute && router.currentRoute.path) || ''
|
||||
const isHomePage = currentPath.includes('/homePage') || currentPath.includes('/ncmatchHome')
|
||||
if (isHomePage) {
|
||||
window.dispatchEvent(new Event('kboss-open-login-dialog'))
|
||||
isRedirectingToLogin = false
|
||||
} else {
|
||||
router.push({path: '/login'}).finally(() => {
|
||||
isRedirectingToLogin = false
|
||||
})
|
||||
}
|
||||
window.dispatchEvent(new Event('kboss-open-login-dialog'))
|
||||
isRedirectingToLogin = false
|
||||
}
|
||||
// window.location.href = 'https://www.opencomputing.cn/'
|
||||
} else if (code == 403) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -112,13 +112,20 @@
|
||||
v-for="(product, index) in displayedTokenProducts"
|
||||
:key="product.id"
|
||||
class="token-market-card"
|
||||
@click="goModelDetail(product)"
|
||||
>
|
||||
>
|
||||
<!-- @click="goModelDetail(product)" -->
|
||||
|
||||
<div class="token-card-top">
|
||||
<span class="token-provider-avatar">
|
||||
<img v-if="product.model_logo" :src="getModelLogoUrl(product.model_logo)" alt="">
|
||||
<img
|
||||
v-if="product.model_logo && !product.logoLoadFailed"
|
||||
:src="getModelLogoUrl(product.model_logo)"
|
||||
alt=""
|
||||
@error="handleModelLogoError(product)"
|
||||
>
|
||||
<span v-else>{{ getProviderInitial(product.provider || product.display_name || product.model_name) }}</span>
|
||||
</span>
|
||||
|
||||
<div class="token-title-group">
|
||||
<h3>{{ product.display_name || product.model_name }}</h3>
|
||||
<p>{{ product.provider || product.model_name || '-' }}</p>
|
||||
@ -126,25 +133,14 @@
|
||||
</div>
|
||||
|
||||
<div class="token-price-grid">
|
||||
<div class="token-price-item">
|
||||
<span>输入</span>
|
||||
<strong>¥{{ formatTokenPrice(product.input_token_price) }}</strong>
|
||||
<em>元/百万 tokens</em>
|
||||
</div>
|
||||
<div class="token-price-item">
|
||||
<span>输出</span>
|
||||
<strong>¥{{ formatTokenPrice(product.output_token_price) }}</strong>
|
||||
<em>元/百万 tokens</em>
|
||||
</div>
|
||||
<div class="token-price-item">
|
||||
<span>缓存读</span>
|
||||
<strong>¥{{ formatTokenPrice(product.cache_hit_input_price) }}</strong>
|
||||
<em>元/百万 tokens</em>
|
||||
</div>
|
||||
<div class="token-price-item">
|
||||
<span>缓存创建</span>
|
||||
<strong>¥{{ formatTokenPrice(product.input_token_price) }}</strong>
|
||||
<em>元/百万 tokens</em>
|
||||
<div
|
||||
v-for="(price, priceIndex) in parseBillingMethod(product.billing_method)"
|
||||
:key="`${product.id}-billing-${priceIndex}`"
|
||||
class="token-price-item"
|
||||
>
|
||||
<span>{{ price.name }}</span>
|
||||
<strong>{{ price.price }}</strong>
|
||||
<em>{{ price.desc }}</em>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -395,6 +391,28 @@ export default {
|
||||
return num.toFixed(4).replace(/\.?0+$/, '');
|
||||
},
|
||||
|
||||
parseBillingMethod(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (!text) {
|
||||
return [{ name: '计费方式', price: '按量计费', desc: '详情以后端配置为准' }];
|
||||
}
|
||||
return text
|
||||
.split(/\n+/)
|
||||
.map(line => line.replace(/^\s*[-–—]\s*/, '').trim())
|
||||
.filter(Boolean)
|
||||
.map(line => {
|
||||
const match = line.match(/^([^::]+)[::]\s*([^\[]+)(?:\[(.+)\])?$/);
|
||||
if (!match) {
|
||||
return { name: '计费说明', price: line, desc: '' };
|
||||
}
|
||||
return {
|
||||
name: match[1].trim(),
|
||||
price: match[2].trim(),
|
||||
desc: (match[3] || '').trim()
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// 供应商头像取首字,避免没有 logo 时卡片显得空。
|
||||
getProviderInitial(provider) {
|
||||
return provider ? provider.slice(0, 1) : 'M';
|
||||
@ -408,6 +426,10 @@ export default {
|
||||
return `${this.IMG_URL}${modelLogo}`;
|
||||
},
|
||||
|
||||
handleModelLogoError(product) {
|
||||
this.$set(product, 'logoLoadFailed', true);
|
||||
},
|
||||
|
||||
normalizeTokenConfigList(value) {
|
||||
if (!value) return [];
|
||||
let parsedValue = value;
|
||||
@ -1471,13 +1493,29 @@ export default {
|
||||
.token-price-grid {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px 12px;
|
||||
max-height: 178px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
padding-right: 4px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.token-price-item {
|
||||
padding: 7px 9px;
|
||||
background: rgba(248, 250, 252, 0.86);
|
||||
border: 1px solid #edf1f7;
|
||||
border-radius: 10px;
|
||||
|
||||
span,
|
||||
em {
|
||||
display: block;
|
||||
@ -1488,19 +1526,24 @@ export default {
|
||||
span {
|
||||
margin-bottom: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
color: #111827;
|
||||
font-size: 15px;
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
em {
|
||||
margin-top: 0;
|
||||
margin-top: 2px;
|
||||
font-size: 10px;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1558,8 +1601,8 @@ export default {
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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