Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5487e12477 | |||
| cb8c442c1d | |||
| 5309bff769 | |||
| c2d2eeee51 | |||
| a8285156e1 | |||
| cb9228f38f | |||
| 749894c3e9 | |||
| 03b9cf2fff | |||
| 3f584172be | |||
| 82341ca054 | |||
| 10a61a8993 | |||
| f3f2fb3c78 | |||
| a51187d14a | |||
| 434e41386f | |||
| ff8f3fef99 | |||
| 86a2d0f472 | |||
| a8bb1140cb | |||
| f23e483a97 | |||
| 377082156c | |||
| fb459ece4d | |||
| b2e195892b | |||
| 4cec84b8ae | |||
| dace3f7a98 | |||
| 6f820050d7 | |||
| c51a2a7d49 | |||
| e35b34ca69 | |||
| 6ca0452581 | |||
| 76e00284d0 | |||
| dd0c598a2c | |||
| 03d412be5d | |||
| 32e7d2482c | |||
| 85c332828c | |||
| cc8b60a677 | |||
| c857194f48 | |||
| 6bc1a2c9f5 | |||
| f5bf42404f | |||
| 9aeaf5109a | |||
| 21e237af80 | |||
| bada9b3e7a | |||
| a343bb4bec | |||
| edc40f6184 | |||
| a0edbffd59 | |||
| 24b8bed619 | |||
| 205bd078ab | |||
| a9e6e8cf29 | |||
| 0312afa7a5 | |||
| e2c778d1d6 | |||
| 75475b3854 | |||
| 07d53c7a68 | |||
| 0cf32d95e4 | |||
| 3aeadf7d80 | |||
| 4e0b59106a | |||
| 78a3ebee03 | |||
| fe649ed106 | |||
| 774eaee961 | |||
| 5fb098b8a4 | |||
| 8963d964df | |||
| 24ba5d5f99 | |||
| db3ac603bc | |||
| 87d670d31f | |||
| 331930ab9c | |||
| e9a3fe5ad1 | |||
| 016efdc69d | |||
| 9f3e5c6bbf | |||
| 01396a032d | |||
| f4f29682ae | |||
| 060d85907c | |||
| c0b3aa9d4d | |||
| a1694102e5 | |||
| bf9202329f | |||
| d3e3ec2b4c | |||
| b2d76b3779 | |||
| 6deaa0e895 | |||
| 3761d9fac3 | |||
| ff09b19505 | |||
| 43d989bf75 | |||
| ea4d909c6a | |||
| 0b91ae22ad | |||
| d5cd8fac00 | |||
| 4601e30cb2 | |||
| da332aca43 | |||
| 2a3dbce9a8 | |||
| 9fef2edaf3 | |||
| 47532b852c | |||
| a697b2d47f | |||
| 8d42d6e700 | |||
| d4dd639698 | |||
| 9af9cfac98 | |||
| b0ac314d35 | |||
| 8bb2ae4af5 | |||
| dd79e637c6 | |||
| 908a76c24b | |||
| 5c30bc18ab | |||
| a2294d7696 | |||
| eee68c844f | |||
| 3bbbc8276f | |||
| c2b740a191 | |||
| 7e4f4003b3 | |||
| 894ad83e8b | |||
| 43131137b8 | |||
| cff6c7d201 | |||
| 737c323ddc | |||
| 109e1156c8 | |||
| 9e5d046660 | |||
| d62737332e | |||
| 9593408770 | |||
| 48c8d1fe90 | |||
| 1df6a0846e | |||
| 90d62845ca | |||
| aa884c7e44 | |||
| 1147e35310 | |||
| 791dca13d3 | |||
| c538e41f10 | |||
| 01bff309f0 | |||
| 6e941ea5e0 | |||
| 80683ad2bd | |||
| 628255ee52 | |||
| e37c6898df | |||
| b0276ac445 | |||
| a23259133a | |||
| 96a1c397b9 | |||
| fc29e9705c | |||
| 0fecf5102d | |||
| a31cfda3f9 | |||
| 178fadd4d7 | |||
| 1029c1e013 | |||
| 55bc7a390b | |||
| 88877fa6f7 | |||
| e69209938d | |||
| 2c062ae2d7 | |||
| 52ce51e00b | |||
| 7ff8481e70 | |||
| b80910376b | |||
| dbe007f81b | |||
| 793c8f6d00 | |||
| 2673cf5052 | |||
| 50b8b66a97 | |||
| f18655e3f7 | |||
| 6eab3d929a | |||
| 1e8e4edb68 | |||
| c8842beb31 | |||
| 390ede6ef8 | |||
| 419f494050 | |||
| 5b879788cb | |||
| 23729c0dba |
@ -35,8 +35,6 @@ async def get_user_balance(ns={}):
|
|||||||
}
|
}
|
||||||
orgid = await sor.R('organization', {'id': user[0]['orgid']})
|
orgid = await sor.R('organization', {'id': user[0]['orgid']})
|
||||||
balance = await getCustomerBalance(sor, orgid[0]['id'])
|
balance = await getCustomerBalance(sor, orgid[0]['id'])
|
||||||
ns['balance'] = balance
|
|
||||||
debug(ns)
|
|
||||||
return {
|
return {
|
||||||
'status': 'ok',
|
'status': 'ok',
|
||||||
'balance': balance
|
'balance': balance
|
||||||
|
|||||||
@ -1,94 +0,0 @@
|
|||||||
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,38 +3,22 @@ def _escape(value):
|
|||||||
return None
|
return None
|
||||||
return str(value).replace("'", "''")
|
return str(value).replace("'", "''")
|
||||||
|
|
||||||
# 客户侧返回字段结构保持与原 model_management 版本一致
|
# 客户侧可见字段(不含 listing_status、is_active 等运营字段)
|
||||||
_CUSTOMER_MODEL_COLUMNS = """
|
_CUSTOMER_MODEL_COLUMNS = """
|
||||||
id,
|
id, llmid, provider, model_name, display_name, model_logo, model_type,
|
||||||
id AS llmid,
|
context_length, input_token_price, output_token_price,
|
||||||
provider_name AS provider,
|
cache_hit_input_price, billing_method, billing_unit,
|
||||||
model AS model_name,
|
capabilities, limitations, highlights, description, sort_order,is_active, experience
|
||||||
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):
|
def _customer_listed_conditions(ns):
|
||||||
"""已上架且启用的模型;支持按厂商、模型类别筛选"""
|
"""已上架且启用的模型;支持按厂商、模型类别筛选"""
|
||||||
conditions = ["status = 'published'"]
|
conditions = ["listing_status = 1", "is_active = 1"]
|
||||||
if ns.get('provider'):
|
if ns.get('provider'):
|
||||||
conditions.append("provider_name = '%s'" % _escape(ns.get('provider')))
|
conditions.append("provider = '%s'" % _escape(ns.get('provider')))
|
||||||
if ns.get('model_type'):
|
if ns.get('model_type'):
|
||||||
conditions.append("catelogname = '%s'" % _escape(ns.get('model_type')))
|
conditions.append("model_type = '%s'" % _escape(ns.get('model_type')))
|
||||||
return ' AND '.join(conditions)
|
return ' AND '.join(conditions)
|
||||||
|
|
||||||
async def model_management_customer_search(ns={}):
|
async def model_management_customer_search(ns={}):
|
||||||
@ -60,27 +44,27 @@ async def model_management_customer_search(ns={}):
|
|||||||
current_page = int(ns.get('current_page', 1))
|
current_page = int(ns.get('current_page', 1))
|
||||||
offset = (current_page - 1) * page_size
|
offset = (current_page - 1) * page_size
|
||||||
where_clause = _customer_listed_conditions(ns)
|
where_clause = _customer_listed_conditions(ns)
|
||||||
listed_base = "status = 'published'"
|
listed_base = "listing_status = 1 AND is_active = 1"
|
||||||
|
|
||||||
db = DBPools()
|
db = DBPools()
|
||||||
async with db.sqlorContext('kboss') as sor:
|
async with db.sqlorContext('kboss') as sor:
|
||||||
try:
|
try:
|
||||||
provider_sql = """
|
provider_sql = """
|
||||||
SELECT DISTINCT provider_name AS provider FROM llm
|
SELECT DISTINCT provider FROM model_management
|
||||||
WHERE %s AND provider_name IS NOT NULL AND provider_name != ''
|
WHERE %s AND provider IS NOT NULL AND provider != ''
|
||||||
ORDER BY provider;
|
ORDER BY provider;
|
||||||
""" % listed_base
|
""" % listed_base
|
||||||
model_type_sql = """
|
model_type_sql = """
|
||||||
SELECT DISTINCT catelogname AS model_type FROM llm
|
SELECT DISTINCT model_type FROM model_management
|
||||||
WHERE %s AND catelogname IS NOT NULL AND catelogname != ''
|
WHERE %s AND model_type IS NOT NULL AND model_type != ''
|
||||||
ORDER BY model_type;
|
ORDER BY model_type;
|
||||||
""" % listed_base
|
""" % listed_base
|
||||||
|
|
||||||
count_sql = """
|
count_sql = """
|
||||||
SELECT COUNT(*) AS total_count FROM llm WHERE %s;
|
SELECT COUNT(*) AS total_count FROM model_management WHERE %s;
|
||||||
""" % where_clause
|
""" % where_clause
|
||||||
find_sql = """
|
find_sql = """
|
||||||
SELECT %s FROM llm
|
SELECT %s FROM model_management
|
||||||
WHERE %s
|
WHERE %s
|
||||||
ORDER BY sort_order ASC
|
ORDER BY sort_order ASC
|
||||||
LIMIT %s OFFSET %s;
|
LIMIT %s OFFSET %s;
|
||||||
|
|||||||
@ -1,182 +1,5 @@
|
|||||||
async def sync_model_to_llm(ns={}):
|
async def sync_model_to_llm(ns={}):
|
||||||
import aiohttp
|
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
|
# 从数据库读取domain和Bearer token
|
||||||
db = DBPools()
|
db = DBPools()
|
||||||
@ -190,13 +13,6 @@ async def sync_model_to_llm(ns={}):
|
|||||||
}
|
}
|
||||||
domain = domain_li[0]['pvalue']
|
domain = domain_li[0]['pvalue']
|
||||||
bearer_token = user_key[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"
|
url = f"{domain}/llmage/list_llms"
|
||||||
header = {
|
header = {
|
||||||
@ -216,93 +32,38 @@ async def sync_model_to_llm(ns={}):
|
|||||||
# 插入数据库
|
# 插入数据库
|
||||||
db = DBPools()
|
db = DBPools()
|
||||||
async with db.sqlorContext('kboss') as sor:
|
async with db.sqlorContext('kboss') as sor:
|
||||||
provider_groups = {}
|
new_llms_count = 0
|
||||||
current_model_set = set()
|
new_llms_list = []
|
||||||
for category_list in result:
|
for category_list in result:
|
||||||
for item in category_list.get('llms', []):
|
for item in category_list.get('llms', []):
|
||||||
provider_orgcode = item.get('providerid')
|
# 查找数据库中是否已经存在,不存在就插入
|
||||||
model = item.get('model')
|
exist_llm = await sor.R('llm', {'model': item.get('model')})
|
||||||
if not provider_orgcode or not model:
|
if exist_llm:
|
||||||
continue
|
continue
|
||||||
item['catelogname'] = item.get('catelogname') or category_list.get('catelogname')
|
new_llms = {
|
||||||
item['catelog_id'] = item.get('catelog_id') or item.get('catelogid') or category_list.get('catelogid')
|
'id': item.get('id'),
|
||||||
current_model_set.add(model)
|
'name': item.get('name'),
|
||||||
if provider_orgcode not in provider_groups:
|
'model': item.get('model'),
|
||||||
provider_groups[provider_orgcode] = {
|
'description': item.get('description'),
|
||||||
'provider_name': item.get('provider_name') or provider_orgcode or '模型供应商',
|
'llmcatelogid': item.get('catelog_id') or item.get('catelogid'),
|
||||||
'items': []
|
'iconid': item.get('iconid'),
|
||||||
}
|
'upappid': item.get('upappid'),
|
||||||
elif item.get('provider_name'):
|
'apiname': item.get('apiname'),
|
||||||
provider_groups[provider_orgcode]['provider_name'] = item.get('provider_name')
|
'providerid': item.get('providerid'),
|
||||||
provider_groups[provider_orgcode]['items'].append({
|
'ownerid': item.get('ownerid') or '0',
|
||||||
'item': item,
|
'enabled_date': item.get('enabled_date'),
|
||||||
'category': category_list
|
'expired_date': item.get('expired_date'),
|
||||||
})
|
'query_apiname': item.get('query_apiname') or '',
|
||||||
|
'query_period': item.get('query_period'),
|
||||||
provider_count = 0
|
'ppid': item.get('ppid'),
|
||||||
product_insert_count = 0
|
}
|
||||||
product_update_count = 0
|
new_llms_count += 1
|
||||||
llm_insert_count = 0
|
new_llms_list.append(new_llms.get('model'))
|
||||||
llm_update_count = 0
|
await sor.C('llm', new_llms)
|
||||||
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 {
|
return {
|
||||||
'status': True,
|
'status': True,
|
||||||
'msg': 'sync_llm_list同步模型成功',
|
'msg': f"sync_llm_list同步模型成功,共插入{new_llms_count}个模型,模型列表: {new_llms_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:
|
except Exception as e:
|
||||||
|
|||||||
145
b/llm.sql
145
b/llm.sql
@ -1,145 +0,0 @@
|
|||||||
# 创建表格命令
|
|
||||||
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,6 +82,14 @@ async def providerAdd(ns={}):
|
|||||||
'status': False,
|
'status': False,
|
||||||
'msg': 'provider add failed, orgcode is empty'
|
'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'):
|
if not ns.get('orgid'):
|
||||||
orgid_li = await sor.R('users', {'id': await get_user()})
|
orgid_li = await sor.R('users', {'id': await get_user()})
|
||||||
orgid = orgid_li[0].get('orgid') if orgid_li else ''
|
orgid = orgid_li[0].get('orgid') if orgid_li else ''
|
||||||
@ -93,33 +101,8 @@ async def providerAdd(ns={}):
|
|||||||
else:
|
else:
|
||||||
orgid = ns.get('orgid')
|
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相同
|
# 供应商parentid 和 运营机构id相同
|
||||||
ns_org['parentid'] = orgid
|
ns_org['parentid'] = orgid
|
||||||
ns_sale_mode['bid_orgid'] = orgid
|
|
||||||
|
|
||||||
if not ns_provider.get('start_date'):
|
if not ns_provider.get('start_date'):
|
||||||
ns_provider['start_date'] = time.strftime('%Y-%m-%d %H:%M:%S')
|
ns_provider['start_date'] = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
@ -132,13 +115,7 @@ async def providerAdd(ns={}):
|
|||||||
await openProviderAccounts(sor, orgid, ns_org['id'])
|
await openProviderAccounts(sor, orgid, ns_org['id'])
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"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:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|||||||
@ -27,16 +27,15 @@ async def reseller_add_user(ns):
|
|||||||
try:
|
try:
|
||||||
ns['id'] = uuid()
|
ns['id'] = uuid()
|
||||||
ns['password'] = password_encode(ns['password'])
|
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:
|
if not tenant_orgid:
|
||||||
return {'status': False, 'msg': '未找到用户所属租户'}
|
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_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, {})
|
user = await sor.sqlExe(user_sql, {})
|
||||||
if len(user) >= 1:
|
if len(user) >= 1:
|
||||||
return {'status': False, 'msg': '用户名重复'}
|
return {'status': False, 'msg': '用户名重复'}
|
||||||
rolereacs = await sor.R('role', {'role': '管理员', 'del_flg': '0', 'org_type': ns.get('org_type')})
|
rolereacs = await sor.R('role', {'role': '管理员', 'del_flg': '0', 'org_type': '1'})
|
||||||
ns['user_reseller'] = ns.get('org_type')
|
ns['user_reseller'] = '1'
|
||||||
ns['tenant_orgid'] = tenant_orgid
|
ns['tenant_orgid'] = tenant_orgid
|
||||||
await sor.C('users', ns)
|
await sor.C('users', ns)
|
||||||
await sor.C('userrole', {'userid': ns['id'], 'roleid': rolereacs[0]['id'], 'id': uuid()})
|
await sor.C('userrole', {'userid': ns['id'], 'roleid': rolereacs[0]['id'], 'id': uuid()})
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,90 +0,0 @@
|
|||||||
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,10 +4,6 @@
|
|||||||
<router-view/>
|
<router-view/>
|
||||||
<FloatingBox
|
<FloatingBox
|
||||||
v-if="!$route.path.includes('kyyForm')&&!$route.path.includes('floatingBox')&&!$route.path.includes('screen')&&!$route.path.includes('wxPage')&&!$route.path.includes('wxDetailPage')"/>
|
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>
|
||||||
<!-- 弹出通话提示-->
|
<!-- 弹出通话提示-->
|
||||||
<div v-if="isShowVideo" class="is-accepted" style="padding: 5px;border-radius: 6px">
|
<div v-if="isShowVideo" class="is-accepted" style="padding: 5px;border-radius: 6px">
|
||||||
@ -42,7 +38,6 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
import FloatingBox from './components/FloatingBox/FloatingBox.vue'; // 导入悬浮框组件
|
import FloatingBox from './components/FloatingBox/FloatingBox.vue'; // 导入悬浮框组件
|
||||||
import LoginDialog from '@/views/LoginDialog/LoginDialog.vue'
|
|
||||||
import '@/assets/elementStyle.css'
|
import '@/assets/elementStyle.css'
|
||||||
import Hammer from 'hammerjs';
|
import Hammer from 'hammerjs';
|
||||||
import {mapState} from "vuex";
|
import {mapState} from "vuex";
|
||||||
@ -50,7 +45,6 @@ export default {
|
|||||||
name: "App",
|
name: "App",
|
||||||
components: {
|
components: {
|
||||||
FloatingBox, // 注册悬浮框组件
|
FloatingBox, // 注册悬浮框组件
|
||||||
LoginDialog,
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -58,8 +52,7 @@ export default {
|
|||||||
socket: null,
|
socket: null,
|
||||||
userId: sessionStorage.getItem('userId'),
|
userId: sessionStorage.getItem('userId'),
|
||||||
vedioObj: {},
|
vedioObj: {},
|
||||||
customerName: '',
|
customerName: ''
|
||||||
loginDialogVisible: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -73,10 +66,6 @@ export default {
|
|||||||
}
|
}
|
||||||
this.setRemUnit()
|
this.setRemUnit()
|
||||||
this.initHammer();
|
this.initHammer();
|
||||||
window.addEventListener('kboss-open-login-dialog', this.openGlobalLoginDialog)
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
window.removeEventListener('kboss-open-login-dialog', this.openGlobalLoginDialog)
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
@ -86,16 +75,6 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
playAudio() {
|
||||||
console.log("开始按钮点击了")
|
console.log("开始按钮点击了")
|
||||||
this.audioElement.play();
|
this.audioElement.play();
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
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,13 +2305,6 @@ 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",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -7,7 +7,6 @@ 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';
|
||||||
@ -24,8 +23,6 @@ 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'];
|
||||||
|
|
||||||
@ -353,30 +350,6 @@ 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)
|
||||||
@ -522,13 +495,6 @@ 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)
|
||||||
@ -536,7 +502,6 @@ 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. 普通用户再补一些固定入口,比如订单、资源、客户专属菜单。
|
||||||
|
|||||||
@ -165,8 +165,16 @@ service.interceptors.response.use(
|
|||||||
isRedirectingToLogin = true
|
isRedirectingToLogin = true
|
||||||
saveLoginRedirectPath()
|
saveLoginRedirectPath()
|
||||||
showErrorMessage('登录已过期,请重新登录')
|
showErrorMessage('登录已过期,请重新登录')
|
||||||
window.dispatchEvent(new Event('kboss-open-login-dialog'))
|
const currentPath = (router.currentRoute && router.currentRoute.path) || ''
|
||||||
isRedirectingToLogin = false
|
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.location.href = 'https://www.opencomputing.cn/'
|
// window.location.href = 'https://www.opencomputing.cn/'
|
||||||
} else if (code == 403) {
|
} else if (code == 403) {
|
||||||
|
|||||||
@ -382,11 +382,6 @@ 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')
|
||||||
|
|||||||
@ -544,7 +544,6 @@ 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() {
|
||||||
|
|||||||
@ -243,7 +243,7 @@ export default {
|
|||||||
this.stopObserveEditorFullscreen()
|
this.stopObserveEditorFullscreen()
|
||||||
this.editorFullscreen = false
|
this.editorFullscreen = false
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
createEmptyForm() {
|
createEmptyForm() {
|
||||||
// 创建新增文章时使用的默认表单结构。
|
// 创建新增文章时使用的默认表单结构。
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -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,12 +19,40 @@
|
|||||||
></el-alert>
|
></el-alert>
|
||||||
|
|
||||||
<section class="doc-hero">
|
<section class="doc-hero">
|
||||||
<h1>元境 API 文档</h1>
|
<h1>{{ apiDoc.model_name }} API 文档</h1>
|
||||||
<p>统一展示大模型、视频、图像、音频、任务查询等接口说明。</p>
|
<p>{{ heroDescription }}</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 markdown-section">
|
<section v-loading="loading" class="doc-section">
|
||||||
<div v-html="renderedMarkdown"></div>
|
<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>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -33,137 +61,124 @@
|
|||||||
</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: '',
|
||||||
markdownText: '',
|
apiDoc: {
|
||||||
markdownUrl: 'https://token.opencomputing.cn/docs/api_doc_zh.md'
|
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())`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
renderedMarkdown() {
|
apiUrlText() {
|
||||||
return this.renderMarkdown(this.markdownText)
|
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 两种接入方式。`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.fetchMarkdownDocument()
|
this.fetchApiDocument()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$route.query.id'() {
|
||||||
|
this.fetchApiDocument()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async fetchMarkdownDocument() {
|
// 通过路由传入的 id 获取模型 API 文档数据。
|
||||||
|
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 response = await fetch(this.markdownUrl)
|
const res = await reqModelApiDocument({ id })
|
||||||
if (!response.ok) {
|
if (res && res.status && res.data) {
|
||||||
throw new Error(`HTTP ${response.status}`)
|
this.apiDoc = {
|
||||||
|
...this.apiDoc,
|
||||||
|
...res.data,
|
||||||
|
model_name: res.data.model_name || this.apiDoc.model_name
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
this.markdownText = await response.text()
|
this.errorMessage = (res && res.msg) || 'API 文档数据获取失败'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[API文档] 获取 Markdown 文档失败', error)
|
console.error('[API文档] 获取模型 API 文档失败', error)
|
||||||
this.errorMessage = 'API 文档加载失败,请稍后重试'
|
this.errorMessage = 'API 文档数据获取失败,请稍后重试'
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
escapeHtml(text = '') {
|
getUrlFromCurl(curlCode) {
|
||||||
return String(text)
|
if (!curlCode) return ''
|
||||||
.replace(/&/g, '&')
|
const match = String(curlCode).match(/https?:\/\/[^\s\\]+/)
|
||||||
.replace(/</g, '<')
|
return match ? match[0] : ''
|
||||||
.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() {
|
goBack() {
|
||||||
this.$router.back()
|
this.$router.back()
|
||||||
@ -260,103 +275,8 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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: 14px 0 24px;
|
margin: 0;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
color: #e6edf3;
|
color: #e6edf3;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|||||||
@ -112,20 +112,13 @@
|
|||||||
v-for="(product, index) in displayedTokenProducts"
|
v-for="(product, index) in displayedTokenProducts"
|
||||||
:key="product.id"
|
:key="product.id"
|
||||||
class="token-market-card"
|
class="token-market-card"
|
||||||
>
|
@click="goModelDetail(product)"
|
||||||
<!-- @click="goModelDetail(product)" -->
|
>
|
||||||
|
|
||||||
<div class="token-card-top">
|
<div class="token-card-top">
|
||||||
<span class="token-provider-avatar">
|
<span class="token-provider-avatar">
|
||||||
<img
|
<img v-if="product.model_logo" :src="getModelLogoUrl(product.model_logo)" alt="">
|
||||||
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 v-else>{{ getProviderInitial(product.provider || product.display_name || product.model_name) }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="token-title-group">
|
<div class="token-title-group">
|
||||||
<h3>{{ product.display_name || product.model_name }}</h3>
|
<h3>{{ product.display_name || product.model_name }}</h3>
|
||||||
<p>{{ product.provider || product.model_name || '-' }}</p>
|
<p>{{ product.provider || product.model_name || '-' }}</p>
|
||||||
@ -133,14 +126,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="token-price-grid">
|
<div class="token-price-grid">
|
||||||
<div
|
<div class="token-price-item">
|
||||||
v-for="(price, priceIndex) in parseBillingMethod(product.billing_method)"
|
<span>输入</span>
|
||||||
:key="`${product.id}-billing-${priceIndex}`"
|
<strong>¥{{ formatTokenPrice(product.input_token_price) }}</strong>
|
||||||
class="token-price-item"
|
<em>元/百万 tokens</em>
|
||||||
>
|
</div>
|
||||||
<span>{{ price.name }}</span>
|
<div class="token-price-item">
|
||||||
<strong>{{ price.price }}</strong>
|
<span>输出</span>
|
||||||
<em>{{ price.desc }}</em>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -391,28 +395,6 @@ export default {
|
|||||||
return num.toFixed(4).replace(/\.?0+$/, '');
|
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 时卡片显得空。
|
// 供应商头像取首字,避免没有 logo 时卡片显得空。
|
||||||
getProviderInitial(provider) {
|
getProviderInitial(provider) {
|
||||||
return provider ? provider.slice(0, 1) : 'M';
|
return provider ? provider.slice(0, 1) : 'M';
|
||||||
@ -426,10 +408,6 @@ export default {
|
|||||||
return `${this.IMG_URL}${modelLogo}`;
|
return `${this.IMG_URL}${modelLogo}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleModelLogoError(product) {
|
|
||||||
this.$set(product, 'logoLoadFailed', true);
|
|
||||||
},
|
|
||||||
|
|
||||||
normalizeTokenConfigList(value) {
|
normalizeTokenConfigList(value) {
|
||||||
if (!value) return [];
|
if (!value) return [];
|
||||||
let parsedValue = value;
|
let parsedValue = value;
|
||||||
@ -1493,29 +1471,13 @@ export default {
|
|||||||
.token-price-grid {
|
.token-price-grid {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
max-height: 178px;
|
display: grid;
|
||||||
display: flex;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
flex-direction: column;
|
gap: 8px 12px;
|
||||||
gap: 5px;
|
|
||||||
margin-bottom: 10px;
|
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 {
|
.token-price-item {
|
||||||
padding: 7px 9px;
|
|
||||||
background: rgba(248, 250, 252, 0.86);
|
|
||||||
border: 1px solid #edf1f7;
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
span,
|
span,
|
||||||
em {
|
em {
|
||||||
display: block;
|
display: block;
|
||||||
@ -1526,24 +1488,19 @@ export default {
|
|||||||
span {
|
span {
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
display: block;
|
display: block;
|
||||||
color: #0f172a;
|
color: #111827;
|
||||||
font-size: 13px;
|
font-size: 15px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
line-height: 1.25;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
em {
|
em {
|
||||||
margin-top: 2px;
|
margin-top: 0;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 1.4;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1601,8 +1558,8 @@ export default {
|
|||||||
|
|
||||||
img {
|
img {
|
||||||
display: block;
|
display: block;
|
||||||
width: 34px;
|
width: 100%;
|
||||||
height: 34px;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,619 +0,0 @@
|
|||||||
<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