Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6e0531b94 | |||
| 1c4006eb85 | |||
|
|
8c6bd7307b | ||
|
|
76b7eb82d6 | ||
|
|
2c7023a36d | ||
| 0db474a49a | |||
| ace2d675c3 | |||
| 823dc27462 | |||
|
|
974730d660 | ||
|
|
02fc6dbbdd | ||
| 81964ebf24 | |||
| c99fba24f3 | |||
|
|
d17712b4f6 | ||
|
|
629d9d10ad | ||
| 2b1e381a02 | |||
| aabbc884d6 | |||
| acc827ad04 | |||
| 12773bf632 | |||
|
|
3523227a38 | ||
| 08cc6e012b | |||
| c1bacfd965 | |||
| 56f2300ba3 | |||
|
|
c8f568d8eb | ||
|
|
94b335c3ed | ||
|
|
3784175a34 | ||
|
|
65d5dc0920 | ||
|
|
a66a9ee56c | ||
|
|
15079ad730 | ||
|
|
2dbf1a859e | ||
|
|
09449c8a4b | ||
| 1de17afa1c | |||
| ac9a17e183 | |||
| 88bf3fa39a | |||
|
|
64c6e50880 | ||
|
|
e75b308f8d | ||
| c9df800c40 | |||
| 965f21554d | |||
| 69b810121d | |||
| 9940350c00 | |||
| 84f6eb5dd2 | |||
| 22e411293f | |||
| 57349ceeeb |
@ -449,7 +449,7 @@ async def get_baidu_orderlist(ns={}):
|
||||
# 获取余额
|
||||
user_balance = await getCustomerBalance(sor, orgid[0]['id'])
|
||||
# 判断余额是否大于50
|
||||
if user_balance < 5000000:
|
||||
if user_balance < 500:
|
||||
await sor.rollback()
|
||||
paydata = {'queryAccountId': baidu_users[0]['baidu_id'], 'orderIds': [ns.get('order_id')]}
|
||||
ns_format = '&'.join(['%s=%s' % (k, v) for k, v in ns.items()])
|
||||
|
||||
@ -170,7 +170,11 @@ async def sync_model_to_llm(ns={}):
|
||||
|
||||
async def upsert_llm(sor, item):
|
||||
llm_data = build_llm_data(item)
|
||||
exist_llm = await sor.R('llm', {'model': item.get('model')})
|
||||
# 厂商 id 是 llm 表主键,优先按 id 更新,避免模型名称变化后重复插入主键
|
||||
exist_llm = await sor.R('llm', {'id': llm_data.get('id')})
|
||||
if not exist_llm:
|
||||
# 兼容历史数据:相同 model 可能已使用其他本地主键
|
||||
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)
|
||||
|
||||
@ -1,3 +1,34 @@
|
||||
# 新版本表结构
|
||||
CREATE TABLE `enterprise_news_article` (
|
||||
`id` varchar(32) NOT NULL COMMENT '唯一标识符',
|
||||
`domain_name` varchar(64) NOT NULL COMMENT '所属域名',
|
||||
`title_zh` varchar(100) DEFAULT NULL COMMENT '中文标题',
|
||||
`title_en` varchar(100) DEFAULT NULL COMMENT '英文标题',
|
||||
`summary_zh` varchar(255) DEFAULT NULL COMMENT '中文摘要',
|
||||
`summary_en` varchar(255) DEFAULT NULL COMMENT '英文摘要',
|
||||
`article_type_zh` varchar(20) DEFAULT NULL COMMENT '中文文章类型',
|
||||
`article_type_en` varchar(20) DEFAULT NULL COMMENT '英文文章类型',
|
||||
`content_zh` text DEFAULT NULL COMMENT '中文正文',
|
||||
`content_en` text DEFAULT NULL COMMENT '英文正文',
|
||||
`cover_img_zh` varchar(255) DEFAULT NULL COMMENT '中文封面图片',
|
||||
`cover_img_en` varchar(255) DEFAULT NULL COMMENT '英文封面图片',
|
||||
`publish_time_zh` date DEFAULT NULL COMMENT '中文发布时间',
|
||||
`publish_time_en` date DEFAULT NULL COMMENT '英文发布时间',
|
||||
`title` varchar(100) DEFAULT NULL COMMENT '文章标题',
|
||||
`article_type` varchar(20) DEFAULT NULL COMMENT '文章类型(企业动态/产品动态/行业洞察/活动资讯)',
|
||||
`summary` varchar(255) DEFAULT NULL COMMENT '文章摘要',
|
||||
`cover_img` varchar(255) DEFAULT NULL COMMENT '封面图片',
|
||||
`content` text DEFAULT NULL COMMENT '文章正文',
|
||||
`status` varchar(1) DEFAULT '0' COMMENT '状态(0-草稿/1-已发布)',
|
||||
`publish_time` date DEFAULT NULL COMMENT '发布时间',
|
||||
`read_count` int(11) DEFAULT 0 COMMENT '阅读数据',
|
||||
`del_flg` varchar(1) DEFAULT '0' COMMENT '删除标志(0-正常/1-已删除)',
|
||||
`update_time` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间',
|
||||
`create_at` timestamp NULL DEFAULT current_timestamp() COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='企业文章表';
|
||||
|
||||
# 旧版本表结构
|
||||
CREATE TABLE `enterprise_news_article` (
|
||||
`id` varchar(32) NOT NULL COMMENT '唯一标识符',
|
||||
`domain_name` varchar(64) NOT NULL COMMENT '所属域名',
|
||||
|
||||
@ -11,7 +11,9 @@ async def front_news_detail(ns={}):
|
||||
update_sql = """update enterprise_news_article set read_count = ifnull(read_count, 0) + 1 where id = '%s' and status = '1' and del_flg = '0';""" % ns.get('id')
|
||||
await sor.sqlExe(update_sql, {})
|
||||
search_sql = """
|
||||
select id, title, article_type, summary, cover_img, content, publish_time, read_count
|
||||
select id, title_zh, title_en, article_type_zh, article_type_en,
|
||||
summary_zh, summary_en, cover_img_zh, cover_img_en,
|
||||
content_zh, content_en, publish_time_zh, publish_time_en, read_count
|
||||
from enterprise_news_article
|
||||
where id = '%s' and status = '1' and del_flg = '0';
|
||||
""" % ns.get('id')
|
||||
|
||||
@ -13,10 +13,14 @@ async def front_news_search(ns={}):
|
||||
offset = (current_page - 1) * page_size
|
||||
|
||||
conditions = ["domain_name = '%s'" % domain_name, "status = '1'", "del_flg = '0'"]
|
||||
if ns.get('article_type'):
|
||||
conditions.append("article_type = '%s'" % ns.get('article_type'))
|
||||
if ns.get('title'):
|
||||
conditions.append("title like '%%%%%s%%%%'" % ns.get('title'))
|
||||
if ns.get('article_type_zh'):
|
||||
conditions.append("article_type_zh = '%s'" % ns.get('article_type_zh'))
|
||||
if ns.get('article_type_en'):
|
||||
conditions.append("article_type_en = '%s'" % ns.get('article_type_en'))
|
||||
if ns.get('title_zh'):
|
||||
conditions.append("title_zh like '%%%%%s%%%%'" % ns.get('title_zh'))
|
||||
if ns.get('title_en'):
|
||||
conditions.append("title_en like '%%%%%s%%%%'" % ns.get('title_en'))
|
||||
where_clause = " and ".join(conditions)
|
||||
|
||||
db = DBPools()
|
||||
@ -25,10 +29,12 @@ async def front_news_search(ns={}):
|
||||
count_sql = """select count(*) as total_count from enterprise_news_article where %s;""" % where_clause
|
||||
total_count = (await sor.sqlExe(count_sql, {}))[0]['total_count']
|
||||
search_sql = """
|
||||
select id, title, article_type, summary, cover_img, publish_time, read_count
|
||||
select id, title_zh, title_en, article_type_zh, article_type_en,
|
||||
summary_zh, summary_en, cover_img_zh, cover_img_en,
|
||||
publish_time_zh, publish_time_en, read_count
|
||||
from enterprise_news_article
|
||||
where %s
|
||||
order by publish_time desc, update_time desc
|
||||
order by publish_time_zh desc, update_time desc
|
||||
limit %s offset %s;
|
||||
""" % (where_clause, page_size, offset)
|
||||
result = await sor.sqlExe(search_sql, {})
|
||||
|
||||
@ -4,16 +4,11 @@ async def news_article_add(ns={}):
|
||||
'status': False,
|
||||
'msg': '请传递url_link'
|
||||
}
|
||||
if not ns.get('title'):
|
||||
if not ns.get('title_zh'):
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '请传递标题'
|
||||
'msg': '请传递中文标题'
|
||||
}
|
||||
# if ns.get('article_type') not in ['企业动态', '产品动态', '行业洞察', '活动资讯']:
|
||||
# return {
|
||||
# 'status': False,
|
||||
# 'msg': '文章类型错误'
|
||||
# }
|
||||
|
||||
domain_name = ns.get('url_link').split("//")[1].split("/")[0]
|
||||
if 'localhost' in domain_name:
|
||||
@ -26,13 +21,19 @@ async def news_article_add(ns={}):
|
||||
ns_dic = {
|
||||
'id': uuid(),
|
||||
'domain_name': domain_name,
|
||||
'title': ns.get('title'),
|
||||
'article_type': ns.get('article_type'),
|
||||
'summary': ns.get('summary'),
|
||||
'cover_img': ns.get('cover_img'),
|
||||
'content': ns.get('content'),
|
||||
'title_zh': ns.get('title_zh'),
|
||||
'title_en': ns.get('title_en'),
|
||||
'article_type_zh': ns.get('article_type_zh'),
|
||||
'article_type_en': ns.get('article_type_en'),
|
||||
'summary_zh': ns.get('summary_zh'),
|
||||
'summary_en': ns.get('summary_en'),
|
||||
'cover_img_zh': ns.get('cover_img_zh'),
|
||||
'cover_img_en': ns.get('cover_img_en'),
|
||||
'content_zh': ns.get('content_zh'),
|
||||
'content_en': ns.get('content_en'),
|
||||
'status': status,
|
||||
'publish_time': ns.get('publish_time'),
|
||||
'publish_time_zh': ns.get('publish_time_zh') or None,
|
||||
'publish_time_en': ns.get('publish_time_en') or None,
|
||||
'read_count': 0,
|
||||
'del_flg': '0'
|
||||
}
|
||||
@ -41,8 +42,13 @@ async def news_article_add(ns={}):
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
try:
|
||||
await sor.C('enterprise_news_article', ns_dic)
|
||||
if status == '1' and not ns.get('publish_time'):
|
||||
publish_sql = """update enterprise_news_article set publish_time = current_timestamp() where id = '%s';""" % ns_dic.get('id')
|
||||
if status == '1':
|
||||
publish_sql = """
|
||||
update enterprise_news_article
|
||||
set publish_time_zh = ifnull(publish_time_zh, current_date()),
|
||||
publish_time_en = ifnull(publish_time_en, current_date())
|
||||
where id = '%s';
|
||||
""" % ns_dic.get('id')
|
||||
await sor.sqlExe(publish_sql, {})
|
||||
return {
|
||||
'status': True,
|
||||
|
||||
@ -8,11 +8,15 @@ async def news_article_publish(ns={}):
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
try:
|
||||
publish_time = ns.get('publish_time')
|
||||
if publish_time:
|
||||
publish_sql = """update enterprise_news_article set status = '1', publish_time = '%s' where id = '%s' and del_flg = '0';""" % (publish_time, ns.get('id'))
|
||||
else:
|
||||
publish_sql = """update enterprise_news_article set status = '1' where id = '%s' and del_flg = '0';""" % ns.get('id')
|
||||
publish_time_zh = "'%s'" % ns.get('publish_time_zh') if ns.get('publish_time_zh') else "ifnull(publish_time_zh, current_date())"
|
||||
publish_time_en = "'%s'" % ns.get('publish_time_en') if ns.get('publish_time_en') else "ifnull(publish_time_en, current_date())"
|
||||
publish_sql = """
|
||||
update enterprise_news_article
|
||||
set status = '1',
|
||||
publish_time_zh = %s,
|
||||
publish_time_en = %s
|
||||
where id = '%s' and del_flg = '0';
|
||||
""" % (publish_time_zh, publish_time_en, ns.get('id'))
|
||||
await sor.sqlExe(publish_sql, {})
|
||||
return {
|
||||
'status': True,
|
||||
|
||||
@ -14,10 +14,14 @@ async def news_article_search(ns={}):
|
||||
|
||||
conditions = ["domain_name = '%s'" % domain_name, "del_flg = '0'"]
|
||||
summary_conditions = ["domain_name = '%s'" % domain_name, "del_flg = '0'"]
|
||||
if ns.get('title'):
|
||||
conditions.append("title like '%%%%%s%%%%'" % ns.get('title'))
|
||||
if ns.get('article_type'):
|
||||
conditions.append("article_type = '%s'" % ns.get('article_type'))
|
||||
if ns.get('title_zh'):
|
||||
conditions.append("title_zh like '%%%%%s%%%%'" % ns.get('title_zh'))
|
||||
if ns.get('title_en'):
|
||||
conditions.append("title_en like '%%%%%s%%%%'" % ns.get('title_en'))
|
||||
if ns.get('article_type_zh'):
|
||||
conditions.append("article_type_zh = '%s'" % ns.get('article_type_zh'))
|
||||
if ns.get('article_type_en'):
|
||||
conditions.append("article_type_en = '%s'" % ns.get('article_type_en'))
|
||||
if ns.get('status'):
|
||||
conditions.append("status = '%s'" % ns.get('status'))
|
||||
where_clause = " and ".join(conditions)
|
||||
@ -27,30 +31,53 @@ async def news_article_search(ns={}):
|
||||
try:
|
||||
count_sql = """select count(*) as total_count from enterprise_news_article where %s;""" % where_clause
|
||||
total_count = (await sor.sqlExe(count_sql, {}))[0]['total_count']
|
||||
summary_sql = """
|
||||
select article_type, count(*) as article_count, ifnull(sum(read_count), 0) as read_count
|
||||
summary_zh_sql = """
|
||||
select article_type_zh as article_type, count(*) as article_count,
|
||||
ifnull(sum(read_count), 0) as read_count
|
||||
from enterprise_news_article
|
||||
where %s
|
||||
group by article_type;
|
||||
and article_type_zh is not null and article_type_zh != ''
|
||||
group by article_type_zh;
|
||||
""" % " and ".join(summary_conditions)
|
||||
summary_result = await sor.sqlExe(summary_sql, {})
|
||||
summary_mapping = {}
|
||||
for summary_dic in summary_result:
|
||||
summary_mapping[summary_dic.get('article_type')] = summary_dic
|
||||
article_type_summary = []
|
||||
summary_en_sql = """
|
||||
select article_type_en as article_type, count(*) as article_count,
|
||||
ifnull(sum(read_count), 0) as read_count
|
||||
from enterprise_news_article
|
||||
where %s
|
||||
and article_type_en is not null and article_type_en != ''
|
||||
group by article_type_en
|
||||
order by article_type_en;
|
||||
""" % " and ".join(summary_conditions)
|
||||
summary_zh_result = await sor.sqlExe(summary_zh_sql, {})
|
||||
summary_en_result = await sor.sqlExe(summary_en_sql, {})
|
||||
summary_zh_mapping = {}
|
||||
for summary_dic in summary_zh_result:
|
||||
summary_zh_mapping[summary_dic.get('article_type')] = summary_dic
|
||||
article_type_summary_zh = []
|
||||
for article_type in ['企业动态', '产品动态', '行业洞察', '活动资讯']:
|
||||
summary_dic = summary_mapping.get(article_type, {})
|
||||
article_type_summary.append({
|
||||
summary_dic = summary_zh_mapping.get(article_type, {})
|
||||
article_type_summary_zh.append({
|
||||
'article_type': article_type,
|
||||
'article_count': summary_dic.get('article_count', 0),
|
||||
'read_count': summary_dic.get('read_count', 0)
|
||||
})
|
||||
article_type_summary_en = []
|
||||
for summary_dic in summary_en_result:
|
||||
article_type_summary_en.append({
|
||||
'article_type': summary_dic.get('article_type'),
|
||||
'article_count': summary_dic.get('article_count', 0),
|
||||
'read_count': summary_dic.get('read_count', 0)
|
||||
})
|
||||
search_sql = """
|
||||
select id, domain_name, title, article_type, summary, cover_img, status, content,
|
||||
publish_time, read_count, update_time, create_at
|
||||
select id, domain_name, title_zh, title_en, article_type_zh, article_type_en,
|
||||
summary_zh, summary_en, cover_img_zh, cover_img_en, content_zh, content_en,
|
||||
status, publish_time_zh, publish_time_en, read_count, update_time, create_at
|
||||
from enterprise_news_article
|
||||
where %s
|
||||
order by publish_time desc, update_time desc
|
||||
order by greatest(
|
||||
ifnull(publish_time_zh, '1000-01-01'),
|
||||
ifnull(publish_time_en, '1000-01-01')
|
||||
) desc, update_time desc
|
||||
limit %s offset %s;
|
||||
""" % (where_clause, page_size, offset)
|
||||
result = await sor.sqlExe(search_sql, {})
|
||||
@ -61,7 +88,8 @@ async def news_article_search(ns={}):
|
||||
'status': True,
|
||||
'msg': 'search news article success',
|
||||
'data': result,
|
||||
'article_type_summary': article_type_summary,
|
||||
'article_type_summary': article_type_summary_zh,
|
||||
'article_type_summary_en': article_type_summary_en,
|
||||
'pagination': {
|
||||
'total': total_count,
|
||||
'page_size': page_size,
|
||||
|
||||
@ -8,21 +8,16 @@ async def news_article_update(ns={}):
|
||||
ns_dic = {
|
||||
'id': ns.get('id')
|
||||
}
|
||||
if 'title' in ns:
|
||||
ns_dic['title'] = ns.get('title')
|
||||
if 'article_type' in ns:
|
||||
# if ns.get('article_type') not in ['企业动态', '产品动态', '行业洞察', '活动资讯']:
|
||||
# return {
|
||||
# 'status': False,
|
||||
# 'msg': '文章类型错误'
|
||||
# }
|
||||
ns_dic['article_type'] = ns.get('article_type')
|
||||
if 'summary' in ns:
|
||||
ns_dic['summary'] = ns.get('summary')
|
||||
if 'cover_img' in ns:
|
||||
ns_dic['cover_img'] = ns.get('cover_img')
|
||||
if 'content' in ns:
|
||||
ns_dic['content'] = ns.get('content')
|
||||
for field in [
|
||||
'title_zh', 'title_en', 'article_type_zh', 'article_type_en',
|
||||
'summary_zh', 'summary_en', 'content_zh', 'content_en',
|
||||
'cover_img_zh', 'cover_img_en', 'publish_time_zh', 'publish_time_en'
|
||||
]:
|
||||
if field in ns:
|
||||
if field in ['publish_time_zh', 'publish_time_en']:
|
||||
ns_dic[field] = ns.get(field) or None
|
||||
else:
|
||||
ns_dic[field] = ns.get(field)
|
||||
if 'status' in ns:
|
||||
if ns.get('status') not in ['0', '1']:
|
||||
return {
|
||||
@ -30,15 +25,17 @@ async def news_article_update(ns={}):
|
||||
'msg': '状态错误'
|
||||
}
|
||||
ns_dic['status'] = ns.get('status')
|
||||
if 'publish_time' in ns:
|
||||
ns_dic['publish_time'] = ns.get('publish_time')
|
||||
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
try:
|
||||
await sor.U('enterprise_news_article', ns_dic)
|
||||
if ns.get('status') == '1' and not ns.get('publish_time'):
|
||||
publish_sql = """update enterprise_news_article set publish_time = ifnull(publish_time, current_timestamp()) where id = '%s';""" % ns.get('id')
|
||||
if ns.get('status') == '1':
|
||||
publish_sql = """
|
||||
update enterprise_news_article
|
||||
set publish_time_zh = ifnull(publish_time_zh, current_date()),
|
||||
publish_time_en = ifnull(publish_time_en, current_date())
|
||||
where id = '%s';
|
||||
""" % ns.get('id')
|
||||
await sor.sqlExe(publish_sql, {})
|
||||
return {
|
||||
'status': True,
|
||||
|
||||
@ -4,7 +4,7 @@ async def search_user_inquiry_dict(ns={}):
|
||||
where_sql = "where status = 1"
|
||||
if ns.get('dict_type'):
|
||||
where_sql += " and dict_type = '%s'" % ns.get('dict_type')
|
||||
search_sql = """select id, dict_type, dict_key, dict_value, sort_order from product_inquiry_dict %s order by dict_type asc, sort_order asc;""" % where_sql
|
||||
search_sql = """select * from product_inquiry_dict %s order by dict_type asc, sort_order asc;""" % where_sql
|
||||
result = await sor.sqlExe(search_sql, {})
|
||||
return {
|
||||
'status': True,
|
||||
|
||||
@ -66,7 +66,9 @@ async def get_ipc_logo(ns={}):
|
||||
"home": {
|
||||
"logoImg": "https://www.opencomputing.cn/idfile?path=logo_ncmatch.png",
|
||||
"bannerTitle": "开元数智",
|
||||
"adress": "北京市石景山区和平西路60号院1号楼11层1101-30",
|
||||
"address": "北京市石景山区和平西路60号院1号楼11层1101-30",
|
||||
"address_zh": "北京市石景山区和平西路60号院1号楼11层1101-30",
|
||||
"address_en": "Room 1101-30, 11th Floor, Building 1, No. 60 Heping West Road, Shijingshan District, Beijing",
|
||||
"footerTitle": "开元数智(北京)科技有限公司",
|
||||
'qrCode': 'https://www.opencomputing.cn/idfile?path=firstpagehot/ncmatch_inquiry.jpg',
|
||||
'footer_info': '京ICP备2022001945号-4 开元数智(北京)科技有限公司',
|
||||
@ -75,7 +77,7 @@ async def get_ipc_logo(ns={}):
|
||||
"email": "zhangjian@bjzai.org.cn",
|
||||
"license": "2022001945号-4",
|
||||
"logo": "https://www.opencomputing.cn/idfile?path=logo_ncmatch.png",
|
||||
"mobile": "13601380912",
|
||||
# "mobile": "13601380912",
|
||||
"publicsecurity": "11010502054007",
|
||||
"businesslicense": "京B2-20232313"
|
||||
},
|
||||
@ -97,7 +99,9 @@ async def get_ipc_logo(ns={}):
|
||||
"home": {
|
||||
"logoImg": "https://zgcopc.opencomputing.cn/idfile?path=logo_zgcopc.png",
|
||||
"bannerTitle": "中关村国际孵化器",
|
||||
"adress": "北京市海淀区上地信息路2号",
|
||||
"address": "北京市海淀区上地信息路2号",
|
||||
"address_zh": "北京市海淀区上地信息路2号",
|
||||
"address_en": "Building 2, No. 2 Xindao Road, Shangdi, Haidian District, Beijing",
|
||||
"footerTitle": "中关村国际孵化器有限公司",
|
||||
'qrCode': 'https://zgcopc.opencomputing.cn/idfile?path=firstpagehot/ncmatch_inquiry.jpg',
|
||||
'footer_info': '京ICP备13007859号-1 北京中关村国际孵化器有限公司',
|
||||
@ -106,7 +110,7 @@ async def get_ipc_logo(ns={}):
|
||||
"email": "zhangjian@bjzai.org.cn",
|
||||
"license": "京ICP备13007859号-1",
|
||||
"logo": "https://zgcopc.opencomputing.cn/idfile?path=logo_zgcopc.png",
|
||||
"mobile": "13601380912",
|
||||
# "mobile": "13601380912",
|
||||
"publicsecurity": "11010502054007",
|
||||
"businesslicense": "京ICP备13007859号-1"
|
||||
},
|
||||
@ -127,7 +131,9 @@ async def get_ipc_logo(ns={}):
|
||||
"home": {
|
||||
"logoImg": "https://www.opencomputing.cn/idfile?path=logo.png",
|
||||
"bannerTitle": "开元云",
|
||||
"adress": "北京市朝阳区东三环中路65号富力中心",
|
||||
"address": "北京市朝阳区东三环中路65号富力中心",
|
||||
"address_zh": "北京市朝阳区东三环中路65号富力中心",
|
||||
"address_en": "R&F Center, 65 East 3rd Ring Middle Rd, Chaoyang, Beijing",
|
||||
"footerTitle": "开元云(北京)科技有限公司",
|
||||
'qrCode': 'https://www.opencomputing.cn/idfile?path=firstpagehot/kaiyuancloud_inquiry.png',
|
||||
'footer_info': '京公网安备11010502054007号 开元云(北京)科技有限公司',
|
||||
|
||||
233
docs/superpowers/plans/2026-07-29-bilingual-enterprise-news.md
Normal file
233
docs/superpowers/plans/2026-07-29-bilingual-enterprise-news.md
Normal file
@ -0,0 +1,233 @@
|
||||
# Bilingual Enterprise News Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Update enterprise news APIs to store, filter, and return Chinese and English article fields without maintaining legacy content fields.
|
||||
|
||||
**Architecture:** Keep the existing DSPY endpoint structure and database table. Add and update endpoints write the eight language-specific columns; search and detail endpoints return those columns directly. Shared publication, image, date, status, and read-count behavior remains unchanged.
|
||||
|
||||
**Tech Stack:** DSPY Python endpoints, async DBPools/sor database access, MySQL.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add and update bilingual content
|
||||
|
||||
**Files:**
|
||||
- Modify: `b/news/news_article_add.dspy`
|
||||
- Modify: `b/news/news_article_update.dspy`
|
||||
|
||||
- [ ] **Step 1: Update add validation**
|
||||
|
||||
Replace the legacy `title` requirement with:
|
||||
|
||||
```python
|
||||
if not ns.get('title_zh'):
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '请传递中文标题'
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update add insert fields**
|
||||
|
||||
Write these fields in `ns_dic`:
|
||||
|
||||
```python
|
||||
'title_zh': ns.get('title_zh'),
|
||||
'title_en': ns.get('title_en'),
|
||||
'article_type_zh': ns.get('article_type_zh'),
|
||||
'article_type_en': ns.get('article_type_en'),
|
||||
'summary_zh': ns.get('summary_zh'),
|
||||
'summary_en': ns.get('summary_en'),
|
||||
'content_zh': ns.get('content_zh'),
|
||||
'content_en': ns.get('content_en'),
|
||||
```
|
||||
|
||||
Do not write `title`, `article_type`, `summary`, or `content`.
|
||||
|
||||
- [ ] **Step 3: Update editable fields**
|
||||
|
||||
In `news_article_update.dspy`, loop through the eight bilingual fields and copy only keys present in `ns`:
|
||||
|
||||
```python
|
||||
for field in [
|
||||
'title_zh', 'title_en', 'article_type_zh', 'article_type_en',
|
||||
'summary_zh', 'summary_en', 'content_zh', 'content_en'
|
||||
]:
|
||||
if field in ns:
|
||||
ns_dic[field] = ns.get(field)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify static diagnostics**
|
||||
|
||||
Run IDE lint diagnostics for both files. Expected: no new errors.
|
||||
|
||||
### Task 2: Update backend search and detail
|
||||
|
||||
**Files:**
|
||||
- Modify: `b/news/news_article_search.dspy`
|
||||
- Verify: `b/news/news_article_detail.dspy`
|
||||
|
||||
- [ ] **Step 1: Add bilingual filters**
|
||||
|
||||
Build optional conditions for exact type matching and fuzzy title matching:
|
||||
|
||||
```python
|
||||
if ns.get('title_zh'):
|
||||
conditions.append("title_zh like '%%%%%s%%%%'" % ns.get('title_zh'))
|
||||
if ns.get('title_en'):
|
||||
conditions.append("title_en like '%%%%%s%%%%'" % ns.get('title_en'))
|
||||
if ns.get('article_type_zh'):
|
||||
conditions.append("article_type_zh = '%s'" % ns.get('article_type_zh'))
|
||||
if ns.get('article_type_en'):
|
||||
conditions.append("article_type_en = '%s'" % ns.get('article_type_en'))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Return bilingual list fields**
|
||||
|
||||
Select:
|
||||
|
||||
```sql
|
||||
id, domain_name,
|
||||
title_zh, title_en,
|
||||
article_type_zh, article_type_en,
|
||||
summary_zh, summary_en,
|
||||
content_zh, content_en,
|
||||
cover_img, status, publish_time, read_count, update_time, create_at
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Return bilingual type summaries**
|
||||
|
||||
Aggregate Chinese and English article types separately and return:
|
||||
|
||||
```python
|
||||
'article_type_summary_zh': article_type_summary_zh,
|
||||
'article_type_summary_en': article_type_summary_en,
|
||||
```
|
||||
|
||||
Each item keeps `article_type`, `article_count`, and `read_count`.
|
||||
|
||||
- [ ] **Step 4: Verify backend detail**
|
||||
|
||||
`news_article_detail.dspy` already uses `select *`; confirm it returns the new columns without changing publication behavior.
|
||||
|
||||
- [ ] **Step 5: Verify static diagnostics**
|
||||
|
||||
Run IDE lint diagnostics for backend search and detail. Expected: no new errors.
|
||||
|
||||
### Task 3: Update frontend search and detail
|
||||
|
||||
**Files:**
|
||||
- Modify: `b/news/front_news_search.dspy`
|
||||
- Modify: `b/news/front_news_detail.dspy`
|
||||
|
||||
- [ ] **Step 1: Add bilingual frontend filters**
|
||||
|
||||
Support `title_zh`, `title_en`, `article_type_zh`, and `article_type_en` using the same matching rules as backend search.
|
||||
|
||||
- [ ] **Step 2: Return bilingual frontend list fields**
|
||||
|
||||
Select:
|
||||
|
||||
```sql
|
||||
id, title_zh, title_en, article_type_zh, article_type_en,
|
||||
summary_zh, summary_en, cover_img, publish_time, read_count
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Return bilingual frontend detail fields**
|
||||
|
||||
After incrementing `read_count`, select:
|
||||
|
||||
```sql
|
||||
id, title_zh, title_en, article_type_zh, article_type_en,
|
||||
summary_zh, summary_en, cover_img, content_zh, content_en,
|
||||
publish_time, read_count
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify publication and read-count behavior**
|
||||
|
||||
Confirm both frontend queries retain `status = '1' AND del_flg = '0'`, and detail still increments `read_count` once.
|
||||
|
||||
- [ ] **Step 5: Verify static diagnostics**
|
||||
|
||||
Run IDE lint diagnostics for both frontend files. Expected: no new errors.
|
||||
|
||||
### Task 4: Final verification
|
||||
|
||||
**Files:**
|
||||
- Verify: `b/news/news_article_add.dspy`
|
||||
- Verify: `b/news/news_article_update.dspy`
|
||||
- Verify: `b/news/news_article_search.dspy`
|
||||
- Verify: `b/news/news_article_detail.dspy`
|
||||
- Verify: `b/news/front_news_search.dspy`
|
||||
- Verify: `b/news/front_news_detail.dspy`
|
||||
|
||||
- [ ] **Step 1: Check legacy-field removal**
|
||||
|
||||
Search the six endpoints for writes or selected output fields named exactly `title`, `article_type`, `summary`, or `content`. Expected: none except comments or compatibility-neutral code.
|
||||
|
||||
- [ ] **Step 2: Check diff scope**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
git diff -- b/news
|
||||
```
|
||||
|
||||
Expected: only the requested bilingual endpoint changes plus the user's existing SQL schema update.
|
||||
|
||||
- [ ] **Step 3: Check whitespace**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
git diff --check -- b/news
|
||||
```
|
||||
|
||||
Expected: no output and exit code 0.
|
||||
|
||||
### Task 5: Add bilingual cover images and publish times
|
||||
|
||||
**Files:**
|
||||
- Modify: `b/news/news_article_add.dspy`
|
||||
- Modify: `b/news/news_article_update.dspy`
|
||||
- Modify: `b/news/news_article_publish.dspy`
|
||||
- Modify: `b/news/news_article_search.dspy`
|
||||
- Modify: `b/news/front_news_search.dspy`
|
||||
- Modify: `b/news/front_news_detail.dspy`
|
||||
|
||||
- [ ] **Step 1: Replace shared write fields**
|
||||
|
||||
Use `cover_img_zh`, `cover_img_en`, `publish_time_zh`, and
|
||||
`publish_time_en` in add and update. Do not write `cover_img` or
|
||||
`publish_time`.
|
||||
|
||||
- [ ] **Step 2: Apply publication defaults**
|
||||
|
||||
When status becomes `1`, set each missing publish-time column with:
|
||||
|
||||
```sql
|
||||
publish_time_zh = ifnull(publish_time_zh, current_date()),
|
||||
publish_time_en = ifnull(publish_time_en, current_date())
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update publish endpoint**
|
||||
|
||||
Accept both publish-time parameters and use `current_date()` for either
|
||||
missing value while setting `status = '1'`.
|
||||
|
||||
- [ ] **Step 4: Update list and detail output**
|
||||
|
||||
Return both cover-image and publish-time columns. Sort lists by:
|
||||
|
||||
```sql
|
||||
greatest(
|
||||
ifnull(publish_time_zh, '1000-01-01'),
|
||||
ifnull(publish_time_en, '1000-01-01')
|
||||
) desc, update_time desc
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Verify**
|
||||
|
||||
Run static field-contract checks, IDE diagnostics, and
|
||||
`git diff --check -- b/news`. Expected: all pass.
|
||||
@ -0,0 +1,44 @@
|
||||
# 中英文企业动态接口设计
|
||||
|
||||
## 目标
|
||||
|
||||
企业文章支持运营同时录入中文和英文内容。接口直接使用
|
||||
`enterprise_news_article` 表中的中英文字段,不再读写旧字段
|
||||
`title`、`article_type`、`summary`、`content`。
|
||||
|
||||
## 字段
|
||||
|
||||
- 中文:`title_zh`、`article_type_zh`、`summary_zh`、`content_zh`
|
||||
- 英文:`title_en`、`article_type_en`、`summary_en`、`content_en`
|
||||
- 中文扩展:`cover_img_zh`、`publish_time_zh`
|
||||
- 英文扩展:`cover_img_en`、`publish_time_en`
|
||||
- 共用:`status`、`read_count`
|
||||
|
||||
新增文章仅要求 `title_zh` 必填,其余中英文字段允许为空。
|
||||
文章发布时,如果未传中英文发布时间,两个字段都默认当天日期。
|
||||
|
||||
## 接口调整
|
||||
|
||||
- `news_article_add.dspy`:接收并写入全部中英文字段。
|
||||
- `news_article_update.dspy`:按传入字段更新中英文内容。
|
||||
- `news_article_search.dspy`:支持中英文标题和类型筛选,返回全部中英文字段;类型汇总分别使用中英文类型。
|
||||
- `news_article_detail.dspy`:返回全部中英文字段。
|
||||
- `front_news_search.dspy`:支持中英文标题和类型筛选,返回全部中英文字段。
|
||||
- `front_news_detail.dspy`:返回全部中英文字段,并保持阅读量加一。
|
||||
- `news_article_publish.dspy`:支持分别设置中英文发布时间,未传时均默认当天。
|
||||
|
||||
下架、删除接口不涉及语言字段,不调整。
|
||||
|
||||
## 兼容边界
|
||||
|
||||
旧字段已由数据库移除非空约束,本次接口不再维护旧字段。调用方需要改为传入和读取中英文新字段。
|
||||
旧字段 `cover_img`、`publish_time` 同样不再读写。列表按两个发布时间中的较晚日期倒序排列,再按更新时间倒序排列。
|
||||
|
||||
## 验证
|
||||
|
||||
- 新增时缺少 `title_zh` 返回校验错误。
|
||||
- 新增、更新后详情能完整返回中英文字段。
|
||||
- 后台和前端列表能按中英文标题、类型筛选。
|
||||
- 新增、编辑、列表和详情能分别读写中英文封面及发布时间。
|
||||
- 发布时未传中英文发布时间,两个字段均写入当天日期。
|
||||
- 前端详情仍仅允许读取已发布文章,并正确增加阅读量。
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
|
||||
26
f/web-kboss/scripts/build_agreement_docs.py
Normal file
26
f/web-kboss/scripts/build_agreement_docs.py
Normal file
@ -0,0 +1,26 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
paras = json.loads((root / 'tmp_user_agreement.json').read_text(encoding='utf-8'))
|
||||
existing = (root / 'src/views/homePage/agreement/agreementDocs.js').read_text(encoding='utf-8')
|
||||
privacy_start = existing.index(" 'privacy': {")
|
||||
privacy_block = existing[privacy_start:]
|
||||
|
||||
lines = [
|
||||
'export default {',
|
||||
" 'user': {",
|
||||
" 'title': '用户协议',",
|
||||
" 'paragraphs': ["
|
||||
]
|
||||
for p in paras:
|
||||
esc = p.replace('\\', '\\\\').replace("'", "\\'")
|
||||
lines.append(f" '{esc}',")
|
||||
lines[-1] = lines[-1].rstrip(',')
|
||||
lines.append(' ]')
|
||||
lines.append(' },')
|
||||
lines.append(privacy_block)
|
||||
|
||||
out = root / 'src/views/homePage/agreement/agreementDocs.js'
|
||||
out.write_text('\n'.join(lines), encoding='utf-8')
|
||||
print(f'Updated {out} with {len(paras)} paragraphs')
|
||||
178
f/web-kboss/scripts/build_agreement_docs_numbered.py
Normal file
178
f/web-kboss/scripts/build_agreement_docs_numbered.py
Normal file
@ -0,0 +1,178 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from pathlib import Path
|
||||
|
||||
USER_PARAGRAPHS = [
|
||||
'<strong>开元云用户协议</strong>',
|
||||
'更新日期:【2026】年【7】月【30】日',
|
||||
'生效日期:【2026】年【7】月【30】日',
|
||||
'本《开元云用户协议》(以下简称“本协议”)由开元云(北京)科技有限公司(以下简称“开元云”或“我们”)与注册、访问或使用开元云服务的自然人、法人或非法人组织(以下简称“用户”或“您”)订立。',
|
||||
'<strong>请您在注册、下单或使用服务前完整阅读本协议。与责任限制、费用、服务暂停或终止、数据处理及争议解决有关的条款已采用加粗方式提示。您点击同意、提交订单、开通或实际使用服务,均表示您已阅读并同意本协议。您代表法人或其他组织操作的,应保证已取得充分授权。</strong>',
|
||||
'<strong>1. 协议范围与定义</strong>',
|
||||
'1.1. 开元云平台,是指由开元云运营的网站、业务平台、客户端、控制台、应用程序、接口及其他服务载体,包括开元算力应用服务平台及后续更名、升级或新增的服务载体。',
|
||||
'1.2. 开元云服务,是指开元云通过平台或双方确认的订单提供的云计算、算力、算力网络、人工智能及相关产品和技术服务,具体以产品页面、订单或服务说明为准。',
|
||||
'1.3. 开元云既可能直接提供服务,也可能代理销售、转售或协助交付第三方服务。实际销售方、服务提供方、开票方及售后责任方,以订单记载为准。',
|
||||
'1.4. 订单,是指用户通过平台提交并由系统确认的订单,以及双方签署或确认的报价单、采购单、服务单、工作说明书或其他交易文件。订单应列明或能够确定服务内容、服务期限、计费方式、实际销售方或服务提供方、开票方、售后责任方及适用的服务规则。',
|
||||
'1.5. 本协议与订单、产品说明、服务等级协议、隐私政策及适用的平台规则共同构成双方协议。约定不一致时,按双方专项书面约定、订单、产品专项规则、本协议的顺序适用;法律另有规定的除外。',
|
||||
'<strong>2. 账户注册与管理</strong>',
|
||||
'2.1. 您应按要求提供真实、准确、完整、有效的注册及认证信息,并在信息变化后及时更新。特定服务依法需要实名核验、资质审查或安全评估的,您应予配合。',
|
||||
'2.2. 您应妥善保管账户及认证信息,并合理设置操作权限。通过您账户实施的操作原则上视为您的行为,但您能够证明账户被未经授权使用且已及时采取合理措施的除外。',
|
||||
'2.3. 发现账户或认证信息被盗用、泄露或存在异常操作时,您应及时通知开元云并采取必要措施。开元云可根据风险采取身份复核、限制登录或临时冻结等措施。',
|
||||
'2.4. 未经开元云同意,您不得转让、出租、出借账户,或者以共享账户等方式规避计费、资质审查或访问控制。',
|
||||
'<strong>3. 服务订购与交付</strong>',
|
||||
'3.1. 服务内容、期限、费用、交付方式、技术指标及支持标准,以产品页面、订单或双方书面约定为准。您应在下单前核对相关信息。',
|
||||
'3.2. 平台展示内容符合要约条件且订单提交成功的,订单成立;订单明确需经资源、资质或合规审核的,以开元云确认、服务开通或双方签署书面文件时成立。',
|
||||
'3.3. 对开元云直接提供的服务,开元云按约承担交付、维护及售后责任。对第三方服务,各方责任依订单及订购前向您提供的第三方规则确定。<strong>开元云的代理或平台角色不免除其依法应承担的信息披露、平台管理及因自身过错产生的责任。</strong>',
|
||||
'3.4. 服务可通过账户开通、资源交付、接口启用、部署完成或订单约定的方式交付。需要验收的,按订单约定办理;订单未约定验收期限的,您应在收到交付通知后五个工作日内完成合理核验并反馈问题,隐蔽缺陷不受该期限限制。',
|
||||
'3.5. 服务变更、扩容、迁移或定制开发,应通过订单、变更单或双方书面确认实施。',
|
||||
'<strong>4. 订购、交付与验收</strong>',
|
||||
'4.1. 您应在下单前核对服务名称、配置、数量、价格、第三方服务商及限制等条件。平台提供订单更正和取消入口的,您可在订单确认前修改;订单确认后的变更、退订或迁移按对应产品规则或双方约定办理。',
|
||||
'4.2. 平台展示的信息符合要约条件且用户提交订单成功的,订单成立;平台明确为要约邀请,或订单需经资源、资质、信用或合规审核的,以开元云确认、资源开通或双方签署书面文件时成立。开元云不得以格式条款约定用户付款后合同仍不成立。',
|
||||
'4.3. 服务通过账户开通、资源交付、许可码发送、接口启用、部署完成或订单约定的其他方式交付。需要验收的,验收标准和期限以订单为准;订单未约定的,用户应在收到交付通知后五个工作日内完成合理核验并反馈可复现的问题,不影响用户依法就隐蔽缺陷主张权利。',
|
||||
'4.4. 按周期续费的服务,以订单是否设置自动续费为准。开元云在自动扣款前应以合理方式提示扣款金额和时间,并提供便捷的取消方式;用户取消后不再产生下一周期费用,但已生效周期按约定继续履行。',
|
||||
'4.5. 资源扩容、迁移、版本升级、服务内容变更或定制开发,应通过新订单、变更单或双方书面确认实施。未经确认的口头沟通不构成对服务范围、费用或期限的变更。',
|
||||
'<strong>5. 费用、支付、发票与退款</strong>',
|
||||
'<strong>5.1. 您应按照订单约定的价格、计费单位、结算周期和支付期限付款。因用户使用量、配置或调用次数变化产生的费用,以平台计量记录和订单计费规则为准;用户对计量结果有异议的,应在账单出具后十五日内提出,开元云应提供合理的核验渠道。</strong>',
|
||||
'5.2. 除订单另有约定,价格不含因用户自身支付方式产生的银行手续费或其他第三方费用。开元云调整服务价格的,应在调整生效前通过平台公告、站内信、电子邮件或订单页面通知;已生效的固定期限订单不受影响,按量服务和续费周期自通知载明的日期适用新价格。',
|
||||
'5.3. 发票由订单或结算页面列明的开票主体依法开具。您应提供真实、准确的开票信息;因信息错误造成的重开、红冲或邮寄成本,由责任方承担。',
|
||||
'<strong>5.4. 退订、退款及未消费余额的处理,以订单和对应产品规则为准。涉及第三方服务的,还受第三方服务商的退订条件和资源采购规则约束。</strong>',
|
||||
'5.5. 用户逾期付款的,开元云可催告并给予合理补救期限;用户在期限届满后仍未付款的,开元云可暂停相应服务并按订单约定收取违约金。暂停前,开元云应在合理可行范围内提示用户备份或迁移数据;紧急安全风险、恶意欠费或法律要求立即暂停的除外。',
|
||||
'<strong>6. 用户数据与个人信息</strong>',
|
||||
'6.1. 用户对其通过服务上传、生成、存储或处理的数据和内容(以下简称“用户数据”)依法享有相应权利。除履行协议、维护安全、遵守法律或取得用户授权外,开元云不取得用户数据的所有权,也不将其用于与提供服务无关的目的。',
|
||||
'6.2. 开元云为账户管理、交易结算、客户服务、安全保障及依法运营而处理个人信息的,按照《开元云隐私政策》执行。',
|
||||
'6.3. 用户决定个人信息处理目的和方式、开元云仅按用户指示提供处理能力的,用户应保证具有合法处理依据;开元云应按照约定和用户的合法指示处理,并采取与风险相适应的安全措施。必要时,双方可另行签署数据处理协议。',
|
||||
'<strong>6.4. 为履行订单确需委托第三方处理或向第三方提供用户数据的,开元云应依法履行告知、合同约束及安全管理义务。未经合法依据,不得向无关第三方提供用户个人信息。</strong>',
|
||||
'6.5. 涉及数据跨境的,由依法负有责任的一方完成相应合规程序。开元云不得在未告知用户的情况下擅自改变订单约定的数据存储地域。',
|
||||
'6.6. 开元云应采取必要的网络和数据安全措施。发生可能影响用户权益的安全事件时,应及时采取补救措施,并按法律规定和合同约定履行通知、报告义务。',
|
||||
'6.7. 服务终止后,开元云按照订单、产品规则或数据处理协议提供合理的数据导出期限,并在期限届满后依法删除或匿名化处理用户数据;依法需要留存的除外。',
|
||||
'<strong>7. 知识产权</strong>',
|
||||
'7.1. 开元云平台、软件、接口、文档、商标及相关技术成果的知识产权归开元云或相应权利人所有。未经许可,用户不得超出订单及产品规则约定的范围使用。',
|
||||
'7.2. 服务期限内,用户获得仅限自身合法业务使用的、非独占且不可转让的使用权。第三方产品或开源组件适用其各自许可条款。',
|
||||
'7.3. 用户数据及用户自行开发成果的权利,依法或依双方约定确定。用户授权开元云在提供和保障服务所必需的范围内处理用户数据。<strong>未经用户另行明确授权,开元云不得使用用户的非公开业务数据训练面向不特定用户的通用模型。</strong>',
|
||||
'7.4. 人工智能或模型服务的输出可能存在不准确或权利瑕疵。用户应结合使用场景进行核验,并依法处理输出内容的使用及权利风险。',
|
||||
'<strong>8. 服务运行与变更</strong>',
|
||||
'8.1. 开元云按照订单或服务等级协议维护其直接提供的服务。第三方服务的可用性、维护及补偿标准,以订单和第三方规则为准;开元云作出更高承诺的,从其承诺。',
|
||||
'8.2. 因维护升级需要计划中断服务的,开元云应按约提前通知;遇有安全事件、重大故障或主管机关要求等紧急情况,可先行处置并及时通知。',
|
||||
'8.3. 开元云可以对服务进行合理升级或调整。涉及主要功能、关键技术指标、数据处理方式或费用的重大变化,应提前合理通知;变化实质影响合同目的的,用户可依法解除受影响的服务。',
|
||||
'8.4. 第三方停止或变更服务导致迁移、替换或退订的,开元云应及时通知并提供合理处理方案。费用和责任按照订单、第三方规则及各方过错确定。',
|
||||
'<strong>9. 服务暂停与终止</strong>',
|
||||
'9.1. 用户可按订单和产品规则申请退订或停止续费。固定期限服务到期且未续费的,服务终止。',
|
||||
'<strong>9.2. 用户逾期付款、严重违反本协议、造成现实安全风险,或者依法需要暂停服务的,开元云可根据风险限制或暂停相关服务。除紧急情形外,开元云应事先通知并给予合理补救期限。</strong>',
|
||||
'9.3. 一方严重违约且在合理期限内未改正的,守约方可解除受影响的订单;违约无法补救或导致重大安全风险的,可立即解除。',
|
||||
'9.4. 服务终止不影响终止前已经产生的付款、保密、数据处理、责任承担及争议解决义务。用户数据按照第6.7条处理。',
|
||||
'<strong>10. 保证、免责与责任限制</strong>',
|
||||
'10.1. 开元云应以符合行业合理标准的技术和管理措施提供服务。除订单、产品说明或法律另有规定外,开元云不保证服务适合用户的特定目的或实现特定结果。',
|
||||
'<strong>10.2. 人工智能、模型、智能分析及计算结果可能存在错误、遗漏或偏差,仅供辅助使用,不能替代专业审查、验证或依法应由人工作出的决定。本条不免除开元云因虚假宣传、违反明示承诺或自身过错依法应承担的责任。</strong>',
|
||||
'10.3. 因用户自身原因或不属于开元云控制范围的第三方原因造成的损失,由责任方依法承担;开元云对其自身过错承担相应责任。',
|
||||
'<strong>10.4. 在法律允许的范围内,一方仅对其违约或过错造成的直接且可合理预见的损失承担责任。除法律规定不得限制的责任以及开元云故意、重大过失外,开元云就单一订单承担的累计赔偿责任,以索赔事件发生前十二个月内用户就该订单项下受影响服务实际支付的费用总额为上限;订单另有约定的,从其约定。</strong>',
|
||||
'10.5. 本协议的免责或限责条款不适用于法律禁止免责或限制责任的情形,也不影响消费者依法享有的权利。',
|
||||
'<strong>11. 保密</strong>',
|
||||
'11.1. 一方因订立或履行本协议知悉的对方非公开技术、经营、数据及其他依其性质应属保密的信息,均为保密信息。接收方仅可为履行本协议使用,并应采取合理保护措施。',
|
||||
'11.2. 已经合法公开、从无保密义务的第三方合法取得、接收方能够证明独立取得,或披露方同意公开的信息,不受前款限制。依法应披露的,接收方可在法定范围内披露,并在法律允许时通知披露方。',
|
||||
'11.3. 保密义务在协议终止后持续五年;商业秘密、个人信息及重要数据依法律规定持续保护。',
|
||||
'<strong>12. 不可抗力与通知</strong>',
|
||||
'12.1. 因不能预见、不能避免且不能克服的事件导致不能履行的,受影响方在法律允许范围内部分或全部免责,但迟延履行后发生不可抗力的除外。受影响方应及时通知并采取合理减损措施。',
|
||||
'12.2. 不可抗力持续超过六十日且导致合同目的不能实现的,任一方可解除受影响的未履行部分,已实际履行的服务按履行情况结算。',
|
||||
'12.3. 开元云可通过平台公告、站内信、账户通知、电子邮件、短信或订单约定的方式发送通知。涉及费用、主要功能、数据处理、服务暂停终止或争议解决的重大事项,应以能够合理到达用户的显著方式通知。',
|
||||
'12.4. 您应及时维护有效联系方式。因您未及时更新导致通知无法送达的,由您承担相应后果;开元云明知联系方式失效仍向该地址发送的除外。',
|
||||
'<strong>12.5. 开元云可因法律变化、服务调整或安全需要修改本协议。对用户权利义务有重大影响的修改,应在生效前显著通知。用户不同意的,可在生效前停止使用并按适用规则终止未履行服务。法律另有规定的,从其规定。</strong>',
|
||||
'<strong>13. 法律适用与其他</strong>',
|
||||
'13.1. 本协议的订立、效力、解释、履行及争议解决适用中华人民共和国大陆地区法律。',
|
||||
'13.2. 双方应先协商解决争议。协商不成的,任一方可向开元云住所地有管辖权的人民法院提起诉讼;消费者依法有权选择其他有管辖权法院的,从其规定。',
|
||||
'13.3. 双方是独立合同主体。本协议不成立合伙、合资、劳动或未经明确授权的代理关系。',
|
||||
'13.4. 一方未行使或迟延行使权利,不构成放弃。部分条款无效或不可执行的,不影响其他条款的效力。',
|
||||
'13.5. 本协议以电子形式订立,与纸质协议具有同等法律效力。用户可通过平台或客服获取、保存和下载协议文本及交易记录。',
|
||||
'13.6. 客服联系方式:【400-6150805 010-65917875】;电子邮箱:【Open-computing@kaiyuancloud.cn】。联系方式变更的,以开元云平台依法公示的信息为准。',
|
||||
]
|
||||
|
||||
PRIVACY_PARAGRAPHS = [
|
||||
'<strong>开元云隐私政策</strong>',
|
||||
'版本更新日期:【2026年7月30日】',
|
||||
'版本生效日期:【2026年7月30日】',
|
||||
'开元云(北京)科技有限公司(以下简称“开元云”或“我们”)重视您的个人信息和隐私保护。本政策适用于我们通过开元云官方网站、开放算力应用服务平台及其他由我们运营并明确适用本政策的产品和服务(统称“开元云服务”)处理个人信息的活动。某项服务另有专门隐私规则的,专门规则优先适用;未约定的事项,适用本政策。',
|
||||
'开元云服务涉及算力服务、算力网络、AI应用、云平台和相关技术服务。您代表单位使用服务的,请确认已取得必要授权;您向我们提供他人个人信息的,应当确保来源合法,并已依法履行告知、取得同意等义务。',
|
||||
'请您在使用开元云服务前阅读本政策。涉及敏感个人信息、向其他个人信息处理者提供个人信息或向境外提供个人信息等依法需要单独同意的事项,我们将另行告知并依法取得您的单独同意。',
|
||||
'<strong>一、定义</strong>',
|
||||
'1. 个人信息:以电子或者其他方式记录的、与已识别或者可识别的自然人有关的各种信息,不包括匿名化处理后的信息。',
|
||||
'2. 敏感个人信息:一旦泄露或者被非法使用,容易导致自然人的人格尊严受到侵害或者人身、财产安全受到危害的个人信息。开元云仅在特定目的、充分必要并采取严格保护措施的情况下处理敏感个人信息。',
|
||||
'3. 匿名化:个人信息经过处理无法识别特定自然人且不能复原的过程。',
|
||||
'4. 用户业务数据:用户在使用开元云服务过程中上传、生成、存储、传输或委托开元云处理的数据,不当然属于个人信息;其中含有个人信息的部分,依照适用法律和双方约定处理。',
|
||||
'<strong>二、我们如何收集和使用个人信息</strong>',
|
||||
'我们遵循合法、正当、必要和诚信原则,仅为明确、合理且与服务直接相关的目的处理个人信息。具体功能所需信息以实际页面、订单、合同或单独告知为准,主要包括:',
|
||||
'1. 账号和联系信息。当您注册、登录或管理账号时,我们可能处理您的账号名称、手机号码、电子邮箱、密码密文及账号安全设置,用于创建账号、身份验证、发送必要通知和保障账号安全。拒绝提供必要信息可能导致无法使用账号功能,但通常不影响浏览公开内容。',
|
||||
'2. 企业认证和交易信息。当您申请试用、购买算力或其他服务、签订或履行合同、结算或开具发票时,我们可能处理单位名称、统一社会信用代码、联系人及联系方式、服务配置、订单、合同、支付状态和开票信息,用于确认交易主体、交付服务、结算、售后和财务管理。支付机构直接处理的银行卡等信息由其依照自身规则处理,我们原则上仅接收完成交易所需的支付结果。',
|
||||
'3. 咨询和服务记录。当您提交咨询、工单、投诉或参加业务活动时,我们可能处理您提交的姓名、联系方式、单位、需求描述、沟通记录和附件,用于回复请求、排查问题和改进服务。非提供服务所必需的调研或推广信息,我们将在必要时另行取得同意,并提供便捷的退订方式。',
|
||||
'4. 设备、日志和安全信息。您访问或使用开元云服务时,我们可能自动记录IP地址、浏览器和设备类型、访问时间、操作记录、故障日志及安全日志,用于运行服务、定位故障、统计基本使用情况和防范网络攻击、欺诈或其他安全风险。我们不会仅因设备或日志信息而不合理限制您的服务。',
|
||||
'5. 依法需要核验的信息。如特定产品、交易或监管要求确需进行个人或企业身份核验,我们可能处理姓名、身份证明或主体资质等必要信息,并在收集前另行说明具体目的、方式、范围和保存期限。涉及敏感个人信息的,我们将采取更严格的保护措施并依法取得单独同意;如有非敏感的替代核验方式,我们将按实际情况提供。',
|
||||
'我们依据您的同意、订立或履行合同所必需、履行法定义务,或者法律规定的其他基础处理个人信息。处理目的、方式或个人信息种类发生变化的,我们将依法重新告知,并在需要时重新取得同意。除法律另有规定外,您可以撤回基于同意作出的授权;撤回不影响撤回前处理活动的效力。',
|
||||
'<strong>三、Cookie和同类技术</strong>',
|
||||
'1. 为保障网站正常运行、保持登录状态、保存必要设置、分析故障和防范安全风险,我们可能使用Cookie和同类技术。必要Cookie被禁用后,部分功能可能无法正常使用;对于非必要Cookie,我们将依法提供选择或关闭方式。',
|
||||
'2. 您可以通过浏览器设置管理或删除Cookie。清除或拒绝Cookie可能使您需要重新登录或设置偏好,但不影响与相关Cookie无关的服务。我们不会将Cookie用于本政策未说明的目的。',
|
||||
'<strong>四、用户业务数据</strong>',
|
||||
'1. 您或您的单位在使用算力、存储、网络、模型、智能文档处理或其他开元云服务时上传、生成、存储、传输或委托我们处理的数据,属于用户业务数据。其中含有个人信息的,您或您的单位通常决定处理目的和方式,我们按照双方协议和您的合法指示提供受托处理服务。',
|
||||
'2. 您应当确保业务数据来源、内容及处理活动合法,并根据适用法律向相关个人履行告知、取得同意等义务。除非产品说明、订单或合同另有明确约定并具备合法处理基础,或者法律另有规定,我们不会将用户业务数据用于自身营销、训练通用模型或其他与提供约定服务无关的目的。',
|
||||
'用户业务数据与我们为账号管理、交易结算、客户支持和安全保障而独立处理的信息,适用不同的责任分工。具体数据位置、备份、迁移、删除和安全要求,以对应产品说明、订单或合同为准。',
|
||||
'<strong>五、委托处理、对外提供、转让和公开披露</strong>',
|
||||
'1. 为提供和保障开元云服务,我们可能委托基础设施、算力、网络、模型接口、身份核验、支付结算、电子签约、发票、客服或安全服务提供方处理必要的个人信息。我们将根据服务性质选择合作方,通过合同约定处理目的、期限、方式、信息种类、安全措施和双方责任,并进行必要监督。合作方不得将受托信息用于自身目的。',
|
||||
'2. 如需向其他个人信息处理者提供您的个人信息,我们将依法告知接收方信息、处理目的、方式和个人信息种类,并在法律要求时取得您的单独同意。涉及敏感个人信息的,我们还会说明必要性及对个人权益的影响。我们不会出售个人信息。',
|
||||
'3. 发生合并、分立、重组、资产转让或类似交易而需要转移个人信息的,我们将向您告知接收方信息,并要求接收方继续履行本政策和法律规定的义务;接收方变更原处理目的或方式的,应当依法重新取得同意。',
|
||||
'4. 我们原则上不公开披露个人信息。确需公开披露的,将告知披露目的、方式和信息种类,依法取得单独同意并采取相应保护措施,法律另有规定的除外。',
|
||||
'<strong>六、个人信息的保存和安全保护</strong>',
|
||||
'1. 我们按照实现处理目的所必要的最短时间保存个人信息;法律法规、监管规定或双方合同另有要求的,从其规定。保存期限届满后,我们将删除个人信息或进行匿名化处理;因技术原因暂时无法删除的,将停止除存储和采取必要安全保护措施之外的处理。',
|
||||
'2. 我们在中华人民共和国境内收集和产生的个人信息原则上存储在境内。具体产品的数据中心和用户业务数据存储位置以产品说明、订单或合同为准。',
|
||||
'3. 我们根据个人信息的种类、处理目的和风险采取与之相适应的安全措施,包括权限控制、身份认证、传输和存储保护、日志审计、备份恢复、人员管理及安全事件处置。互联网环境并非绝对安全,请您妥善保管账号和认证凭证,不要通过不安全渠道发送敏感信息。',
|
||||
'4. 发生或者可能发生个人信息泄露、篡改、丢失时,我们将立即采取补救措施,并依照法律规定向主管部门报告;可能对您的权益造成危害的,我们将依法告知事件情况、可能影响、已采取或拟采取的措施及降低风险的建议。',
|
||||
'<strong>七、您的权利</strong>',
|
||||
'1. 在法律规定范围内,您有权知情、决定、限制或拒绝我们处理您的个人信息,并可请求查阅、复制、更正、补充或删除个人信息,撤回同意、注销账号,以及要求我们解释个人信息处理规则。我们利用个人信息进行自动化决策并对您的权益产生重大影响的,您有权要求说明,并有权拒绝仅通过自动化决策作出的决定。',
|
||||
'2. 您可以通过产品提供的账号设置、工单或本政策所列联系方式提出请求。为保护账号和信息安全,我们可能验证您的身份,并在法律规定的期限内处理。请求明显不合理、超出必要限度,或法律规定可以不予响应的,我们可能拒绝或限制响应,并向您说明理由。',
|
||||
'3. 账号注销后,我们将停止提供与账号相关的服务,并依法删除或匿名化处理相关个人信息;依法需要保留的信息,在保存期间仅用于履行法定义务或争议处理。注销前,请您按照产品说明妥善迁移或备份用户业务数据。',
|
||||
'<strong>八、未成年人个人信息</strong>',
|
||||
'不以不满十四周岁的未成年人应当在父母或其他监护人指导下使用开元云相关服务。我们发现未经监护人同意处理了不满十四周岁未成年人的个人信息时,将依法删除或采取其他必要措施。',
|
||||
'<strong>九、个人信息跨境提供</strong>',
|
||||
'1. 如特定服务需要向中华人民共和国境外提供个人信息,我们将仅在业务确有必要且符合法律规定的情况下进行,并在提供前告知境外接收方信息、处理目的和方式、个人信息种类以及您向境外接收方行使权利的方式和程序。我们将依法履行相应程序,取得单独同意,并要求境外接收方达到法律规定的个人信息保护标准。',
|
||||
'2. 用户自行选择境外资源、境外模型或其他境外服务的,相关数据位置和跨境安排以对应产品说明、订单、合同及单独告知为准。',
|
||||
'<strong>十、本政策的更新</strong>',
|
||||
'1. 我们可能根据法律变化、业务调整或个人信息处理活动变化更新本政策。更新后的政策将通过网站、产品页面或其他适当方式发布。处理目的、方式、个人信息种类、保存期限或个人权利等发生重大变化的,我们将以显著方式通知;依法需要重新取得同意的,将在相关处理前完成。',
|
||||
'2. 未经您的同意,我们不会通过更新本政策减损您依法享有的权利。',
|
||||
'<strong>十一、如何联系我们</strong>',
|
||||
'1. 如您对本政策或个人信息处理活动有疑问、意见、投诉,或需要行使个人信息权利,可通过以下方式联系开元云:',
|
||||
'公司名称:【开元云(北京)科技有限公司】',
|
||||
'联系地址:【北京市朝阳区东三环中路65号富力中心】',
|
||||
'联系电话:【400-6150805 010-65917875】',
|
||||
'联系邮箱:【Open-computing@kaiyuancloud.cn】',
|
||||
'2. 我们将在核验您的身份后依法处理。对处理结果有异议的,您可以再次向我们反馈,也可以依法向履行个人信息保护职责的部门投诉、举报或寻求其他救济。',
|
||||
]
|
||||
|
||||
|
||||
def esc(text: str) -> str:
|
||||
return text.replace('\\', '\\\\').replace("'", "\\'")
|
||||
|
||||
|
||||
def render_paragraphs(items):
|
||||
lines = []
|
||||
for item in items:
|
||||
lines.append(f" '{esc(item)}',")
|
||||
lines[-1] = lines[-1].rstrip(',')
|
||||
return lines
|
||||
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
out = root / 'src/views/homePage/agreement/agreementDocs.js'
|
||||
content = '\n'.join([
|
||||
'export default {',
|
||||
" 'user': {",
|
||||
" 'title': '用户协议',",
|
||||
" 'paragraphs': [",
|
||||
*render_paragraphs(USER_PARAGRAPHS),
|
||||
' ]',
|
||||
' },',
|
||||
" 'privacy': {",
|
||||
" 'title': '隐私政策',",
|
||||
" 'paragraphs': [",
|
||||
*render_paragraphs(PRIVACY_PARAGRAPHS),
|
||||
' ]',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
])
|
||||
out.write_text(content, encoding='utf-8')
|
||||
print(f'Wrote {out}')
|
||||
42
f/web-kboss/scripts/extract_agreement.py
Normal file
42
f/web-kboss/scripts/extract_agreement.py
Normal file
@ -0,0 +1,42 @@
|
||||
import json
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
docx = Path(r'd:\电脑管家迁移文件\xwechat_files\wxid_wfmw9gfr2p9u22_fea0\msg\attach\ca2d7449434be57a2cd16ff8c9e33d13\2026-08\Rec\b548997303412ca6\F\0\开元云用户协议- TY20260729.docx')
|
||||
W = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
|
||||
ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
|
||||
|
||||
|
||||
def run_bold(run):
|
||||
rpr = run.find('w:rPr', ns)
|
||||
if rpr is None:
|
||||
return False
|
||||
bold = rpr.find('w:b', ns)
|
||||
if bold is None:
|
||||
return False
|
||||
val = bold.get(f'{W}val')
|
||||
return val not in ('0', 'false')
|
||||
|
||||
|
||||
with zipfile.ZipFile(docx) as zf:
|
||||
root = ET.fromstring(zf.read('word/document.xml'))
|
||||
|
||||
paragraphs = []
|
||||
for paragraph in root.iter(f'{W}p'):
|
||||
parts = []
|
||||
for run in paragraph.iter(f'{W}r'):
|
||||
text = ''.join(t.text or '' for t in run.iter(f'{W}t'))
|
||||
if not text:
|
||||
continue
|
||||
if run_bold(run):
|
||||
parts.append(f'<strong>{text}</strong>')
|
||||
else:
|
||||
parts.append(text)
|
||||
line = ''.join(parts).strip()
|
||||
if line:
|
||||
paragraphs.append(line)
|
||||
|
||||
out = Path(__file__).resolve().parents[1] / 'tmp_user_agreement.json'
|
||||
out.write_text(json.dumps(paragraphs, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
print(f'Wrote {len(paragraphs)} paragraphs to {out}')
|
||||
28
f/web-kboss/src/api/FinancialSettlementCenter.js
Normal file
28
f/web-kboss/src/api/FinancialSettlementCenter.js
Normal file
@ -0,0 +1,28 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
const get = (url, params = {}) => request({
|
||||
url,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
|
||||
// 汇总查询:accounting_orgid、counterparty_type、period_type、start_date、end_date 等。
|
||||
export const summaryQueryAPI = params => get('/bill/finance_settlement_summary.dspy', params)
|
||||
|
||||
// 结算单预览:对手方和账期参数使用 period_start、period_end。
|
||||
export const settlementPreviewAPI = params => get('/bill/finance_settlement_preview.dspy', params)
|
||||
|
||||
// 截图接口定义为 GET,创建结算单参数通过 query 发送。
|
||||
export const createSettlementStatementAPI = params => get('/bill/finance_settlement_create.dspy', params)
|
||||
|
||||
// 结算单列表:支持对手方、状态、账期和分页筛选。
|
||||
export const settlementListAPI = params => get('/bill/finance_settlement_list.dspy', params)
|
||||
|
||||
// 结算单详情:使用 settlement_id、current_page、page_size。
|
||||
export const settlementDetailsAPI = params => get('/bill/finance_settlement_detail.dspy', params)
|
||||
|
||||
// 截图接口定义为 GET,提交审批参数为 settlement_id、userid、business_name。
|
||||
export const submitApprovalAPI = params => get('/bill/finance_settlement_submit.dspy', params)
|
||||
|
||||
// 截图接口定义为 GET,审批回调参数为 apv_id、status。
|
||||
export const approvalCallbackAPI = params => get('/bill/finance_settlement_apv_callback.dspy', params)
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div class="box">
|
||||
<div>
|
||||
<div class="contener" v-if="shouldShowFloatingConsult">
|
||||
|
||||
<div v-if="shouldShowFloatingConsult" class="contener">
|
||||
|
||||
<transition name="slide">
|
||||
<div v-show="windowsHidden" style="font-size: 14px">
|
||||
@ -10,7 +9,7 @@
|
||||
<a href="#" class="floating-consult-btn primary" @click.prevent="openAiConsult">
|
||||
<div class="floating-consult-icon">
|
||||
<img src="./img/ocai.jpg" alt="在线咨询">
|
||||
<span class="consult-badge"></span>
|
||||
<span class="consult-badge" />
|
||||
</div>
|
||||
<!-- <span class="floating-consult-text">在线咨询</span> -->
|
||||
</a>
|
||||
@ -18,37 +17,41 @@
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 申请试用弹窗 -->
|
||||
<el-dialog title="申请试用产品" :visible.sync="dialogVisible" width="35%" :before-close="handleClose"
|
||||
custom-class="apply-use">
|
||||
<el-dialog
|
||||
title="申请试用产品"
|
||||
:visible.sync="dialogVisible"
|
||||
width="35%"
|
||||
:before-close="handleClose"
|
||||
custom-class="apply-use"
|
||||
>
|
||||
<el-form ref="form" :model="form" label-width="120px" :rules="rules" style="width: 80%">
|
||||
<el-form-item label="姓名:" prop="customer">
|
||||
<el-input v-model="form.customer" placeholder="请输入姓名"></el-input>
|
||||
<el-input v-model="form.customer" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系方式:" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入联系方式"></el-input>
|
||||
<el-input v-model="form.phone" placeholder="请输入联系方式" />
|
||||
</el-form-item>
|
||||
<el-form-item label="公司:" prop="unit">
|
||||
<el-input v-model="form.unit" placeholder="请输入公司名称"></el-input>
|
||||
<el-input v-model="form.unit" placeholder="请输入公司名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="数量:">
|
||||
<el-input disabled v-model="form.product_num" placeholder="请输入资源数量"></el-input>
|
||||
<el-input v-model="form.product_num" disabled placeholder="请输入资源数量" />
|
||||
</el-form-item>
|
||||
<el-form-item label="需求型号:" prop="resources">
|
||||
<!-- <el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4}" v-model="form.resources" placeholder="请输入资源需求"></el-input>-->
|
||||
<el-checkbox-group v-model="form.resources" style="display: flex;flex-wrap: wrap;">
|
||||
<el-checkbox label="H800" style="width: 50%;"></el-checkbox>
|
||||
<el-checkbox label="H100" style="width: 50%;"></el-checkbox>
|
||||
<el-checkbox label="A800" style="width: 50%;"></el-checkbox>
|
||||
<el-checkbox label="A100" style="width: 50%;"></el-checkbox>
|
||||
<el-checkbox label="H800" style="width: 50%;" />
|
||||
<el-checkbox label="H100" style="width: 50%;" />
|
||||
<el-checkbox label="A800" style="width: 50%;" />
|
||||
<el-checkbox label="A100" style="width: 50%;" />
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose" size="small">取 消</el-button>
|
||||
<el-button size="small" @click="handleClose">取 消</el-button>
|
||||
<el-button size="small" type="primary" @click="onSubmit()">确 定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
@ -56,10 +59,10 @@
|
||||
<!-- v-if="isShowChangeChat"-->
|
||||
<div v-if="isShowChangeChat " class="changeChat" style="">
|
||||
<span style="margin-bottom: 5px;display: block">请选择聊天方式</span>
|
||||
<el-checkbox-group :min="1" style="margin-bottom: 5px" v-model="checkType">
|
||||
<el-checkbox label="文字"></el-checkbox>
|
||||
<el-checkbox label="视频"></el-checkbox>
|
||||
<el-checkbox label="音频"></el-checkbox>
|
||||
<el-checkbox-group v-model="checkType" :min="1" style="margin-bottom: 5px">
|
||||
<el-checkbox label="文字" />
|
||||
<el-checkbox label="视频" />
|
||||
<el-checkbox label="音频" />
|
||||
</el-checkbox-group>
|
||||
<div style="display: flex;justify-content: flex-end">
|
||||
<el-button size="mini" @click="closeWindow">关闭</el-button>
|
||||
@ -74,12 +77,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {contantAPI, applyAPI} from "../../api/floatingWindow/index";
|
||||
import axios from "axios";
|
||||
import {mapState} from "vuex";
|
||||
import {getCustomerOfSaleUserId} from "@/api/customer/vedio";
|
||||
import { getHomePath } from "@/views/setting/tools";
|
||||
import { reqNewHomeConsult } from "@/api/newHome";
|
||||
import { contantAPI, applyAPI } from '../../api/floatingWindow/index'
|
||||
import { getCustomerOfSaleUserId } from '@/api/customer/vedio'
|
||||
import { getHomePath } from '@/views/setting/tools'
|
||||
import { reqNewHomeConsult } from '@/api/newHome'
|
||||
|
||||
export default {
|
||||
// 组件的逻辑代码
|
||||
@ -87,10 +88,10 @@ export default {
|
||||
return {
|
||||
socket: null,
|
||||
checkType: ['文字'],
|
||||
isShowChangeChat: false,//控制选择沟通方式弹窗
|
||||
isShowChangeChat: false, // 控制选择沟通方式弹窗
|
||||
loading: true,
|
||||
currentDate: '2021-06-01',
|
||||
isShowPicLoading: true,//图片loading展示
|
||||
isShowPicLoading: true, // 图片loading展示
|
||||
isSend: true,
|
||||
windowsHidden: true,
|
||||
// 获取用户代理字P串
|
||||
@ -98,7 +99,7 @@ export default {
|
||||
// 判断是否为移动设备
|
||||
isMobile: false,
|
||||
url: window.location.href,
|
||||
orgType: "",
|
||||
orgType: '',
|
||||
isShow: false,
|
||||
userid: sessionStorage.getItem('userId'),
|
||||
src: null,
|
||||
@ -114,42 +115,22 @@ export default {
|
||||
},
|
||||
rules: {
|
||||
customer: [
|
||||
{required: true, message: "请输入姓名", trigger: "change"},
|
||||
{ required: true, message: '请输入姓名', trigger: 'change' }
|
||||
],
|
||||
phone: [{pattern: /^1(3\d|4[5-9]|5[0-35-9]|6[567]|7[0-8]|8\d|9[0-35-9])\d{8}$/, message: "请输入有效的手机号"},
|
||||
{required: true, trigger: "blur", message: "请输入手机号"}],
|
||||
phone: [{ pattern: /^1(3\d|4[5-9]|5[0-35-9]|6[567]|7[0-8]|8\d|9[0-35-9])\d{8}$/, message: '请输入有效的手机号' },
|
||||
{ required: true, trigger: 'blur', message: '请输入手机号' }],
|
||||
unit: [
|
||||
{required: true, message: "请输入单位", trigger: 'change'},
|
||||
{ required: true, message: '请输入单位', trigger: 'change' }
|
||||
],
|
||||
product: [
|
||||
{required: true, message: "请输入资源名称", trigger: 'change'},
|
||||
{ required: true, message: '请输入资源名称', trigger: 'change' }
|
||||
],
|
||||
resources: [
|
||||
{required: true, message: "请输入资源需求", trigger: 'blur'},
|
||||
],
|
||||
},
|
||||
{ required: true, message: '请输入资源需求', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(this.userAgent);
|
||||
|
||||
},
|
||||
watch: {
|
||||
'$route': { // $route可以用引号,也可以不用引号
|
||||
handler(to, from) {
|
||||
this.orgType = sessionStorage.getItem('org_type') ? sessionStorage.getItem('org_type') : "";
|
||||
this.userid = sessionStorage.getItem('userId') ? sessionStorage.getItem('userId') : "";
|
||||
},
|
||||
deep: true, // 深度监听
|
||||
immediate: true, // 第一次初始化渲染就可以监听到
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// debugger
|
||||
// console.log("路由展示~~~",location.href)
|
||||
// console.log("路由展示~~~",this.$route)
|
||||
|
||||
},
|
||||
computed: {
|
||||
shouldShowFloatingConsult() {
|
||||
const isHomePage = [
|
||||
@ -157,9 +138,28 @@ export default {
|
||||
'/homePage/indexLast',
|
||||
'/ncmatchHome/index'
|
||||
].includes(this.$route.path)
|
||||
return isHomePage || this.orgType == 2 || this.orgType == 3 || !this.userid
|
||||
return isHomePage || this.orgType === '2' || this.orgType === '3' || !this.userid
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route': { // $route可以用引号,也可以不用引号
|
||||
handler(to, from) {
|
||||
this.orgType = sessionStorage.getItem('org_type') ? sessionStorage.getItem('org_type') : ''
|
||||
this.userid = sessionStorage.getItem('userId') ? sessionStorage.getItem('userId') : ''
|
||||
},
|
||||
deep: true, // 深度监听
|
||||
immediate: true // 第一次初始化渲染就可以监听到
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(this.userAgent)
|
||||
},
|
||||
mounted() {
|
||||
// debugger
|
||||
// console.log("路由展示~~~",location.href)
|
||||
// console.log("路由展示~~~",this.$route)
|
||||
|
||||
},
|
||||
methods: {
|
||||
// 让模板中可以直接调用 getHomePath()
|
||||
getHomePath,
|
||||
@ -168,21 +168,21 @@ export default {
|
||||
title: '提示',
|
||||
message: '当前还未登录,请先登录~',
|
||||
type: 'warning'
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
goVedioPage() {
|
||||
if (!sessionStorage.getItem('userId')) {
|
||||
console.log("未登录,请重新登录")
|
||||
console.log('未登录,请重新登录')
|
||||
this.openTip()
|
||||
} else {
|
||||
let ploady = {
|
||||
const ploady = {
|
||||
audioIsOn: 0,
|
||||
vedioIsOn: 0,
|
||||
chatIsOn: 0,
|
||||
userId: this.userid,
|
||||
toUserId: 0,
|
||||
customerName: sessionStorage.getItem('username'),
|
||||
customerName: sessionStorage.getItem('username')
|
||||
}
|
||||
for (let i = 0; i < this.checkType.length; i++) {
|
||||
if (this.checkType[i] === '文字') {
|
||||
@ -194,9 +194,8 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getCustomerOfSaleUserId({userid: this.userid}).then(res => {
|
||||
let routeData = {
|
||||
getCustomerOfSaleUserId({ userid: this.userid }).then(res => {
|
||||
const routeData = {
|
||||
chatIsOn: ploady.chatIsOn,
|
||||
vedioIsOn: ploady.vedioIsOn,
|
||||
audioIsOn: ploady.audioIsOn,
|
||||
@ -215,9 +214,9 @@ export default {
|
||||
})
|
||||
this.isShowChangeChat = false
|
||||
// let webSocketUrl = 'wss://www.opencomputing.cn/dev/wss/pub/rtcc.ws'
|
||||
let webSocketUrl = 'wss://www.opencomputing.cn/pub/rtcc.ws'
|
||||
const webSocketUrl = 'wss://www.opencomputing.cn/pub/rtcc.ws'
|
||||
this.socket = new WebSocket(webSocketUrl)
|
||||
let ploadyed = {
|
||||
const ploadyed = {
|
||||
type: 'send',
|
||||
from: sessionStorage.getItem('userId'),
|
||||
uuid: res.data,
|
||||
@ -230,36 +229,47 @@ export default {
|
||||
// content: {}
|
||||
// }
|
||||
this.socket.onopen = (event) => {
|
||||
console.log("客户创建成功了@@@@@")
|
||||
console.log("发送的信息是", ploadyed)
|
||||
this.socket.send(JSON.stringify(ploadyed));
|
||||
};
|
||||
console.log('客户创建成功了@@@@@')
|
||||
console.log('发送的信息是', ploadyed)
|
||||
this.socket.send(JSON.stringify(ploadyed))
|
||||
}
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log('接收到消息@@@@:', event.data);
|
||||
};
|
||||
console.log('接收到消息@@@@:', event.data)
|
||||
}
|
||||
|
||||
this.socket.onclose = (event) => {
|
||||
console.log('WebSocket连接已关闭');
|
||||
};
|
||||
console.log('WebSocket连接已关闭')
|
||||
}
|
||||
} else {
|
||||
console.log("zouleels")
|
||||
console.log('zouleels')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
open2() {
|
||||
this.isShowChangeChat = true
|
||||
},
|
||||
getYunbaoProducts() {
|
||||
return [
|
||||
{ id: 'bid', key: this.$t('yunbao.products.bid'), intro: 'E投标智能体是覆盖「招标解析→标书编制→合规审查→查重→知识沉淀」全链路的一站式投标AI解决方案,帮企业将编标周期从数天压缩至小时级,低级废标率降低100%。' },
|
||||
{ id: 'tender', key: this.$t('yunbao.products.tender'), intro: 'E招标智能体,AI辅助编制招标文件,自动合规性审查,让招标流程更高效、更透明、更合规。' },
|
||||
{ id: 'evaluation', key: this.$t('yunbao.products.evaluation'), intro: 'E评标智能体,AI辅助评标分析,自动提取关键指标、横向对比评分、识别异常报价,帮助评标专家高效、公正地完成评审工作。' },
|
||||
{ id: 'loggingReview', key: this.$t('yunbao.products.loggingReview'), intro: '面向林业管理的行业智能体,可在线完成采伐申请材料的智能核验,自动比对采伐范围、树种、蓄积量等核心指标与合规要求,快速识别违规申请,助力林业资源可持续利用。' },
|
||||
{ id: 'gasMonitor', key: this.$t('yunbao.products.gasMonitor'), intro: '能源领域工业智能体,实时采集燃机运行的温度、压力、振动等多维度数据,通过AI算法识别异常运行征兆,提前预判潜在故障,保障燃机稳定运行,降低运维成本。' },
|
||||
{ id: 'powerPrice', key: this.$t('yunbao.products.powerPrice'), intro: '面向能源行业的预测智能体,结合历史电价数据、供需变化、政策调整、天气影响等多维度变量,通过机器学习模型实现不同周期的电价精准预测,帮助电力企业优化收益管理。' },
|
||||
{ id: 'comic', key: this.$t('yunbao.products.comic'), intro: '互联网内容创作智能体,覆盖从剧本生成、分镜拆解、素材生成到视频合成的全流程制作。支持工业化批量生产,帮助中小团队低成本快速产出AI漫画作品,大幅降低创作门槛。' },
|
||||
{ id: 'contract', key: this.$t('yunbao.products.contract'), intro: this.$t('casePages.contract.heroDesc') },
|
||||
{ id: 'decision', key: this.$t('yunbao.products.decision'), intro: this.$t('casePages.decision.heroDesc') }
|
||||
]
|
||||
},
|
||||
openAiConsult() {
|
||||
this.resetYunbaoChatInstance()
|
||||
this.installYunbaoChat()
|
||||
window.YunbaoChat.init({
|
||||
avatarUrl: require('./img/ocai.jpg'),
|
||||
name: '云宝小助手'
|
||||
name: this.$t('yunbao.name'),
|
||||
products: this.getYunbaoProducts()
|
||||
})
|
||||
window.YunbaoChat.open()
|
||||
},
|
||||
@ -287,22 +297,12 @@ export default {
|
||||
this._inited = true
|
||||
this._config = Object.assign({
|
||||
avatarUrl: '',
|
||||
name: '云宝小助手',
|
||||
name: vm.$t('yunbao.name'),
|
||||
submitUrl: '',
|
||||
products: [
|
||||
{ key: 'E投标', intro: 'E投标智能体是覆盖「招标解析→标书编制→合规审查→查重→知识沉淀」全链路的一站式投标AI解决方案,帮企业将编标周期从数天压缩至小时级,低级废标率降低100%。' },
|
||||
{ key: 'E招标', intro: 'E招标智能体,AI辅助编制招标文件,自动合规性审查,让招标流程更高效、更透明、更合规。' },
|
||||
{ key: 'E评标', intro: 'E评标智能体,AI辅助评标分析,自动提取关键指标、横向对比评分、识别异常报价,帮助评标专家高效、公正地完成评审工作。' },
|
||||
{ key: '采伐智审', intro: '面向林业管理的行业智能体,可在线完成采伐申请材料的智能核验,自动比对采伐范围、树种、蓄积量等核心指标与合规要求,快速识别违规申请,助力林业资源可持续利用。' },
|
||||
{ key: '燃机智慧监盘', intro: '能源领域工业智能体,实时采集燃机运行的温度、压力、振动等多维度数据,通过AI算法识别异常运行征兆,提前预判潜在故障,保障燃机稳定运行,降低运维成本。' },
|
||||
{ key: '电价预测', intro: '面向能源行业的预测智能体,结合历史电价数据、供需变化、政策调整、天气影响等多维度变量,通过机器学习模型实现不同周期的电价精准预测,帮助电力企业优化收益管理。' },
|
||||
{ key: 'AI漫剧制作', intro: '互联网内容创作智能体,覆盖从剧本生成、分镜拆解、素材生成到视频合成的全流程制作。支持工业化批量生产,帮助中小团队低成本快速产出AI漫画作品,大幅降低创作门槛。' },
|
||||
{ key: '合同智能审查', intro: 'AI驱动的办公智能体,可快速识别合同中的风险条款、表述歧义、合规漏洞,自动标注问题位置并给出修改建议。支持多类型合同模板适配,大幅缩短人工审查时长。' },
|
||||
{ key: '投策智能体', intro: '面向企业投资决策的辅助智能体,整合多维度市场数据、行业趋势与政策信息,通过算法模拟不同决策场景的收益与风险,自动生成可视化分析报告,提升投资方案科学性。' }
|
||||
],
|
||||
products: vm.getYunbaoProducts(),
|
||||
pages: {
|
||||
'合同智能审查': '/homePage/agentStore/contractCase',
|
||||
'投策智能体': '/homePage/agentStore/decisionCase'
|
||||
contract: '/homePage/agentStore/contractCase',
|
||||
decision: '/homePage/agentStore/decisionCase'
|
||||
}
|
||||
}, config)
|
||||
this.injectCSS()
|
||||
@ -353,9 +353,9 @@ export default {
|
||||
if (!body) return
|
||||
body.innerHTML = ''
|
||||
setTimeout(() => {
|
||||
this.addBotMsg('嗨~我是云宝 👋<br>请问您想随便看看,还是直接聊聊需求?', [
|
||||
{ text: '随便看看', action: 'browse' },
|
||||
{ text: '直接聊聊需求', action: 'direct' }
|
||||
this.addBotMsg(vm.$t('yunbao.welcome'), [
|
||||
{ text: vm.$t('yunbao.browse'), action: 'browse' },
|
||||
{ text: vm.$t('yunbao.direct'), action: 'direct' }
|
||||
])
|
||||
}, 200)
|
||||
},
|
||||
@ -373,7 +373,7 @@ export default {
|
||||
<div class="yb-chat-product-card" data-action="${item.action || ''}" data-param="${item.param || ''}" data-url="${item.url || ''}" data-text="${item.text || ''}">
|
||||
<div class="yb-chat-product-card-title">${item.text}</div>
|
||||
<div class="yb-chat-product-card-desc">${item.desc || ''}</div>
|
||||
<span class="yb-chat-product-card-link">查看更多 →</span>
|
||||
<span class="yb-chat-product-card-link">${vm.$t('yunbao.learnMore')}</span>
|
||||
</div>
|
||||
`).join('')}</div>`
|
||||
} else if (type === 'tags') {
|
||||
@ -383,7 +383,7 @@ export default {
|
||||
}
|
||||
}
|
||||
msg.innerHTML = `
|
||||
<div class="yb-chat-msg-avatar"><img src="${this._config.avatarUrl}" alt="云宝"></div>
|
||||
<div class="yb-chat-msg-avatar"><img src="${this._config.avatarUrl}" alt="${this._config.name}"></div>
|
||||
<div>
|
||||
<div class="yb-chat-msg-bubble">${html}</div>
|
||||
${optionHtml}
|
||||
@ -413,43 +413,43 @@ export default {
|
||||
}
|
||||
if (action === 'browse') {
|
||||
setTimeout(() => {
|
||||
this.addBotMsg('好的~为您推荐我们的热门产品 👇', [
|
||||
{ text: 'E投标', desc: '更智能的标书写作引擎', action: 'goExternal', url: 'https://bid-ocai-v2.jinan.opencomputing.cn/', type: 'cards' },
|
||||
{ text: '合同智能审查', desc: 'AI驱动合同风险审查,效率提升60%+', action: 'goContract', type: 'cards' },
|
||||
{ text: '投策智能体', desc: '分钟级投研分析,精准辅助投资决策', action: 'goInvest', type: 'cards' }
|
||||
this.addBotMsg(vm.$t('yunbao.recommend'), [
|
||||
{ text: vm.$t('yunbao.products.bid'), desc: vm.$t('yunbao.productDescs.bid'), action: 'goExternal', url: 'https://bid-ocai-v2.jinan.opencomputing.cn/', type: 'cards' },
|
||||
{ text: vm.$t('yunbao.products.contract'), desc: vm.$t('yunbao.productDescs.contract'), action: 'goContract', type: 'cards' },
|
||||
{ text: vm.$t('yunbao.products.decision'), desc: vm.$t('yunbao.productDescs.decision'), action: 'goInvest', type: 'cards' }
|
||||
])
|
||||
setTimeout(() => {
|
||||
this.addBotMsg('或者您还有其他想了解的产品吗?', this.getProductTags(), 'tags')
|
||||
this.addBotMsg(vm.$t('yunbao.moreProducts'), this.getProductTags(), 'tags')
|
||||
}, 1200)
|
||||
}, 200)
|
||||
return
|
||||
}
|
||||
if (action === 'direct') {
|
||||
setTimeout(() => {
|
||||
this.addBotMsg('您想了解哪方面的产品或服务呢?', this.getProductTags(), 'tags')
|
||||
this.addBotMsg(vm.$t('yunbao.askProduct'), this.getProductTags(), 'tags')
|
||||
}, 200)
|
||||
return
|
||||
}
|
||||
if (action === 'goContract') {
|
||||
vm.$router.push(this._config.pages['合同智能审查'] || '/homePage/agentStore/contractCase').catch(() => {})
|
||||
vm.$router.push(this._config.pages.contract || '/homePage/agentStore/contractCase').catch(() => {})
|
||||
this.close()
|
||||
setTimeout(() => {
|
||||
this.addBotMsg('已为您打开合同智能审查页面 📄<br>还有其他想了解的吗?', this.getProductTags(), 'tags')
|
||||
this.addBotMsg(vm.$t('yunbao.openedContract'), this.getProductTags(), 'tags')
|
||||
}, 200)
|
||||
return
|
||||
}
|
||||
if (action === 'goExternal') {
|
||||
window.open(url, '_blank')
|
||||
setTimeout(() => {
|
||||
this.addBotMsg('已为您打开相关页面。还有其他想了解的吗?', this.getProductTags(), 'tags')
|
||||
this.addBotMsg(vm.$t('yunbao.openedPage'), this.getProductTags(), 'tags')
|
||||
}, 200)
|
||||
return
|
||||
}
|
||||
if (action === 'goInvest') {
|
||||
vm.$router.push(this._config.pages['投策智能体'] || '/homePage/agentStore/decisionCase').catch(() => {})
|
||||
vm.$router.push(this._config.pages.decision || '/homePage/agentStore/decisionCase').catch(() => {})
|
||||
this.close()
|
||||
setTimeout(() => {
|
||||
this.addBotMsg('已为您打开投策智能体页面 📊<br>还有其他想了解的吗?', this.getProductTags(), 'tags')
|
||||
this.addBotMsg(vm.$t('yunbao.openedDecision'), this.getProductTags(), 'tags')
|
||||
}, 200)
|
||||
return
|
||||
}
|
||||
@ -467,12 +467,16 @@ export default {
|
||||
return this._config.products.map(item => ({
|
||||
text: item.key,
|
||||
action: 'productIntro',
|
||||
param: item.key
|
||||
param: item.id
|
||||
}))
|
||||
},
|
||||
getProductIntro(key) {
|
||||
const product = this._config.products.find(item => item.key === key)
|
||||
return product ? product.intro : '这是一款优秀的AI产品,欢迎进一步了解!'
|
||||
const product = this._config.products.find(item => item.id === key || item.key === key)
|
||||
return product ? product.intro : vm.$t('yunbao.productIntroDefault')
|
||||
},
|
||||
getProductName(key) {
|
||||
const product = this._config.products.find(item => item.id === key || item.key === key)
|
||||
return product ? product.key : key
|
||||
},
|
||||
addContactForm() {
|
||||
const body = document.getElementById('yunbaoChatBody')
|
||||
@ -481,16 +485,16 @@ export default {
|
||||
const form = document.createElement('div')
|
||||
form.className = 'yb-chat-msg'
|
||||
form.innerHTML = `
|
||||
<div class="yb-chat-msg-avatar"><img src="${this._config.avatarUrl}" alt="云宝"></div>
|
||||
<div class="yb-chat-msg-avatar"><img src="${this._config.avatarUrl}" alt="${this._config.name}"></div>
|
||||
<div>
|
||||
<div class="yb-chat-msg-bubble">
|
||||
请留下您的联系方式,我们的顾问将尽快与您联系 😊
|
||||
${vm.$t('yunbao.consultantNote')}
|
||||
<div class="yb-chat-form">
|
||||
<input type="text" class="yb-chat-form-input" id="yunbaoChatName" placeholder="您的姓名">
|
||||
<input type="tel" class="yb-chat-form-input" id="yunbaoChatPhone" placeholder="联系电话">
|
||||
<input type="text" class="yb-chat-form-input" id="yunbaoChatCompany" placeholder="公司名称">
|
||||
<button class="yb-chat-form-submit" id="yunbaoChatSubmit">提交咨询</button>
|
||||
<div class="yb-chat-form-tip">信息仅用于顾问联系</div>
|
||||
<input type="text" class="yb-chat-form-input" id="yunbaoChatName" placeholder="${vm.$t('contactSales.namePlaceholder')}">
|
||||
<input type="tel" class="yb-chat-form-input" id="yunbaoChatPhone" placeholder="${vm.$t('contactSales.phonePlaceholder')}">
|
||||
<input type="text" class="yb-chat-form-input" id="yunbaoChatCompany" placeholder="${vm.$t('contactSales.companyPlaceholder')}">
|
||||
<button class="yb-chat-form-submit" id="yunbaoChatSubmit">${vm.$t('yunbao.submit')}</button>
|
||||
<div class="yb-chat-form-tip">${vm.$t('yunbao.infoNote')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -505,28 +509,29 @@ export default {
|
||||
const company = document.getElementById('yunbaoChatCompany').value.trim()
|
||||
const submitBtn = document.getElementById('yunbaoChatSubmit')
|
||||
if (!name || !phone) {
|
||||
this.addBotMsg('请先填写姓名和联系电话哦~')
|
||||
this.addBotMsg(vm.$t('yunbao.nameRequired'))
|
||||
return
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
this.addBotMsg('请输入正确的 11 位手机号哦~')
|
||||
this.addBotMsg(vm.$t('yunbao.phoneInvalid'))
|
||||
return
|
||||
}
|
||||
if (submitBtn && submitBtn.disabled) return
|
||||
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true
|
||||
submitBtn.innerText = '提交中...'
|
||||
submitBtn.innerText = vm.$t('yunbao.submitting')
|
||||
}
|
||||
|
||||
this.addUserMsg('已提交联系方式')
|
||||
this.addUserMsg(vm.$t('yunbao.submitted'))
|
||||
const productName = this.getProductName(this._selectedProduct)
|
||||
const data = {
|
||||
custom_type: '1',
|
||||
name,
|
||||
phone,
|
||||
company,
|
||||
email: '',
|
||||
content: this._selectedProduct ? `我想咨询关于【${this._selectedProduct}】的产品信息` : '云宝对话咨询',
|
||||
content: this._selectedProduct ? vm.$t('yunbao.consultingContent').replace('{product}', productName) : vm.$t('yunbao.defaultContent'),
|
||||
source: '官网',
|
||||
url_link: window.location.href
|
||||
}
|
||||
@ -534,20 +539,20 @@ export default {
|
||||
try {
|
||||
const response = await reqNewHomeConsult(data)
|
||||
if (response && response.status) {
|
||||
this.addBotMsg('收到!我们的顾问将尽快与您联系 🎉<br>感谢您的信任!')
|
||||
this.addBotMsg(vm.$t('yunbao.success'))
|
||||
return
|
||||
}
|
||||
this.addBotMsg((response && response.msg) || '提交失败了,您可以稍后重试。', [
|
||||
{ text: '重新提交', action: 'retrySubmit', primary: true }
|
||||
this.addBotMsg((response && response.msg) || vm.$t('yunbao.submitFail'), [
|
||||
{ text: vm.$t('yunbao.retry'), action: 'retrySubmit', primary: true }
|
||||
])
|
||||
} catch (error) {
|
||||
this.addBotMsg('网络开小差了,提交失败,您可以重试一次。', [
|
||||
{ text: '重新提交', action: 'retrySubmit', primary: true }
|
||||
this.addBotMsg(vm.$t('yunbao.networkFail'), [
|
||||
{ text: vm.$t('yunbao.retry'), action: 'retrySubmit', primary: true }
|
||||
])
|
||||
} finally {
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false
|
||||
submitBtn.innerText = '提交咨询'
|
||||
submitBtn.innerText = vm.$t('yunbao.submit')
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -558,10 +563,10 @@ export default {
|
||||
<div id="yunbaoModal" class="yb-assistant-modal-overlay">
|
||||
<div class="yb-assistant-modal">
|
||||
<div class="yb-assistant-modal-header">
|
||||
<div class="yb-chat-avatar"><img src="${config.avatarUrl}" alt="云宝"></div>
|
||||
<div class="yb-chat-avatar"><img src="${config.avatarUrl}" alt="${config.name}"></div>
|
||||
<div class="yb-chat-header-info">
|
||||
<div class="yb-chat-header-name">${config.name}</div>
|
||||
<div class="yb-chat-header-status">在线</div>
|
||||
<div class="yb-chat-header-status">${this.$t('yunbao.online')}</div>
|
||||
</div>
|
||||
<button class="yb-assistant-close-btn" id="yunbaoCloseBtn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
@ -626,7 +631,7 @@ export default {
|
||||
@media(max-width:480px){.yb-assistant-modal{width:calc(100vw - 32px);height:calc(100vh - 140px);right:0}.yb-assistant-modal-overlay{right:16px;bottom:90px}}
|
||||
`
|
||||
},
|
||||
//关闭弹窗
|
||||
// 关闭弹窗
|
||||
closeWindow() {
|
||||
this.isShowChangeChat = false
|
||||
},
|
||||
@ -634,14 +639,14 @@ export default {
|
||||
this.isShowPicLoading = false
|
||||
console.log()
|
||||
},
|
||||
handleMouseEnter() {//鼠标移入事件
|
||||
handleMouseEnter() { // 鼠标移入事件
|
||||
this.isShow = true
|
||||
if (this.isSend) {
|
||||
this.contant()
|
||||
}
|
||||
// this.contant()
|
||||
},
|
||||
handleMouseLeave() {//鼠标移出事件
|
||||
handleMouseLeave() { // 鼠标移出事件
|
||||
this.isSend = true
|
||||
this.src = ''
|
||||
this.phone = ''
|
||||
@ -649,11 +654,11 @@ export default {
|
||||
this.isShow = false
|
||||
this.isShowPicLoading = true
|
||||
},
|
||||
contant() {//联系我们
|
||||
let params = {
|
||||
contant() { // 联系我们
|
||||
const params = {
|
||||
// userid: sessionStorage.getItem("userId")
|
||||
}
|
||||
if (this.url.indexOf('www') != -1) {
|
||||
if (this.url.indexOf('www') !== -1) {
|
||||
params.domain = 'www'
|
||||
}
|
||||
contantAPI(params).then(res => {
|
||||
@ -661,70 +666,65 @@ export default {
|
||||
|
||||
this.isShowPicLoading = false
|
||||
if (res.status) {
|
||||
|
||||
this.src = res.picture_url
|
||||
this.phone = res.contactor_phone
|
||||
} else {
|
||||
this.$message({
|
||||
message: res.msg,
|
||||
type: "error",
|
||||
});
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
onSubmit() {//点击确认申请试用
|
||||
onSubmit() { // 点击确认申请试用
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (valid) {
|
||||
let pamars = {
|
||||
const pamars = {
|
||||
customer: this.form.customer,
|
||||
phone: this.form.phone,
|
||||
unit: this.form.unit,
|
||||
resources: this.form.resources,
|
||||
userid: sessionStorage.getItem("userId")
|
||||
userid: sessionStorage.getItem('userId')
|
||||
}
|
||||
applyAPI(pamars).then(res => {
|
||||
if (res.status) {
|
||||
this.dialogVisible = false
|
||||
this.$message({
|
||||
message: "成功申请试用",
|
||||
type: "success",
|
||||
});
|
||||
message: '成功申请试用',
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
this.$message({
|
||||
message: res.msg,
|
||||
type: "error",
|
||||
});
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
tryUse() {//申请试用
|
||||
|
||||
if (sessionStorage.getItem("userId")) {
|
||||
tryUse() { // 申请试用
|
||||
if (sessionStorage.getItem('userId')) {
|
||||
this.dialogVisible = true
|
||||
} else {
|
||||
// window.open('https://www.opencomputing.cn/#/floatingBox')
|
||||
this.$router.push('/floatingBox')
|
||||
}
|
||||
|
||||
},
|
||||
handleClose() {
|
||||
this.$refs.form.resetFields();
|
||||
this.$refs.form.resetFields()
|
||||
this.dialogVisible = false
|
||||
},
|
||||
//是否展示悬浮窗
|
||||
// 是否展示悬浮窗
|
||||
isShowFloating() {
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.box {
|
||||
|
||||
|
||||
.contener {
|
||||
.dialog {
|
||||
width: 400px;
|
||||
@ -1082,13 +1082,11 @@ export default {
|
||||
width: .38rem;
|
||||
flex-direction: column;
|
||||
|
||||
|
||||
&:hover {
|
||||
.cloud-contact-us-i {
|
||||
background-position: -100px 0
|
||||
}
|
||||
|
||||
|
||||
//.active {
|
||||
// border: 5px solid red;
|
||||
// background-color: white !important;
|
||||
@ -1319,7 +1317,6 @@ export default {
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<style>
|
||||
.apply-use {
|
||||
|
||||
@ -18,7 +18,7 @@ export default {
|
||||
switchEnSuccess: 'Switched to English'
|
||||
},
|
||||
home: {
|
||||
heroTitle: 'One Platform · Intelligent Leap · Across All Industries.',
|
||||
heroTitle: 'Make AI Everywhere, Make AI Easy',
|
||||
heroSubtitle: '',
|
||||
heroSlogan: 'Make AI Everywhere, Make AI Easy',
|
||||
solutionsBtn: 'Solutions',
|
||||
@ -33,6 +33,10 @@ export default {
|
||||
viewMore: 'View More →',
|
||||
footerProducts: 'Products & Services',
|
||||
footerContact: 'Contact Us',
|
||||
footerAddress: 'Address',
|
||||
footerEmail: 'Email',
|
||||
footerTel: 'Tel',
|
||||
followOfficialAccount: 'Follow Official Account',
|
||||
onlineChat: 'Online Chat',
|
||||
solutionCards: {
|
||||
collaboration: {
|
||||
@ -160,5 +164,242 @@ export default {
|
||||
secondDesc: 'Recognized for its industrial-grade AI agent factory delivery capabilities and benchmark cases among central and state-owned enterprises, Open Computing AI was named a Forbes China AI Commercial Application Demonstration Enterprise — underscoring its leadership in turning AI technology into scalable, real-world industry solutions.'
|
||||
}
|
||||
},
|
||||
about: {
|
||||
intro: {
|
||||
kicker: 'About Us',
|
||||
title: 'About Us',
|
||||
copyHtml: 'Open Computing AI takes <strong class="copy-strong">"Make AI Everywhere, Make AI Easy"</strong> as its mission. By empowering all industries through integrated technologies, we strive to become a world-leading operator of AI Agent services. The company focuses on key application scenarios including <strong class="copy-strong">education & research, engineering & transportation, port & water utilities, energy & power, and life-science & healthcare</strong>. We build full-stack AI solutions to drive the industrial intelligent transformation. As a Specialized, Refined, Differentiated and Innovative Enterprise, National High-Tech Enterprise, Tech-Based SME and Innovative SME, the company has obtained <span class="copy-nowrap">ISO 9001, ISO 27001 and ISO 28000</span> certifications. In 2026, it was selected as one of Forbes China’s AI Commercial Implementation Demonstration Enterprises. We have established in-depth cooperation with top-tier institutions such as Peking University Science and Technology Park (PKUSTP), Zhongguancun NCMatch, Inspur Yunzhou, China Railway Group Smart City R&D Center, Beibu Gulf Port Big Data, Baidu Intelligent Cloud and Alibaba Cloud. Leveraging our self-developed AI Agent Factory, we have achieved dual breakthroughs in AI model services and commercial value. Open Computing AI will continue to deepen the scenario-based implementation of AI in vertical industries to accelerate the realization of our strategic vision.'
|
||||
},
|
||||
advantages: {
|
||||
kicker: 'Why Choose Us',
|
||||
title: 'Why Choose Open Computing AI',
|
||||
fast: {
|
||||
title: 'Faster',
|
||||
subtitle: 'Ultra-fast inference · Compute efficiency',
|
||||
tag: 'AI Infra',
|
||||
desc: 'Dramatically accelerates model inference and provides a solid computing infrastructure foundation for enterprise efficiency.'
|
||||
},
|
||||
accurate: {
|
||||
title: 'More Accurate',
|
||||
subtitle: 'Vertical adaptation · Truth-oriented results',
|
||||
tag: 'AI Agent',
|
||||
desc: 'Provides model pre-training, dedicated knowledge bases, and ontology services for vertical industry scenarios.'
|
||||
},
|
||||
strong: {
|
||||
title: 'Stronger',
|
||||
subtitle: 'Full-stack capability · Human-AI collaboration',
|
||||
tag: 'AI Ability',
|
||||
desc: 'Brings globally leading AI capability models into real-world scenarios and continuously enhances AI controllability.'
|
||||
},
|
||||
stable: {
|
||||
title: 'More Stable',
|
||||
subtitle: 'Private deployment · Secure and reliable',
|
||||
tag: 'AI Safety',
|
||||
desc: 'Runs large models and agents stably in dedicated private-domain scenarios, backed by national-level professional teams.'
|
||||
}
|
||||
},
|
||||
credentials: {
|
||||
kicker: 'Credentials',
|
||||
title: 'Credentials & Honors',
|
||||
highTech: {
|
||||
title: 'National High-Tech Enterprise',
|
||||
desc: 'An innovative enterprise recognized by the state in high-tech fields'
|
||||
},
|
||||
specialized: {
|
||||
title: 'Specialized, Refined, Differentiated and Innovative Enterprise',
|
||||
desc: 'Specialized, Refined, Differentiated, and Innovative development direction'
|
||||
},
|
||||
techSme: {
|
||||
title: 'Tech-Based SME',
|
||||
desc: 'Certification for small and medium-sized enterprises with technological innovation capabilities'
|
||||
},
|
||||
iso: {
|
||||
title: 'ISO 9001/27001/28000',
|
||||
desc: 'Triple certification in quality management, information security, and supply chain security'
|
||||
},
|
||||
innovative: {
|
||||
title: 'Innovative SME',
|
||||
desc: 'An enterprise with high innovation capability and growth potential'
|
||||
},
|
||||
forbes: {
|
||||
title: 'Forbes China',
|
||||
descPrefix: 'Selected as ',
|
||||
descHighlight: '"Forbes China AI Commercial Implementation Demonstration Enterprise"',
|
||||
descSuffix: ' in 2026'
|
||||
}
|
||||
},
|
||||
cta: {
|
||||
title: 'Join Hands with Open Computing AI to Build an Intelligent Future',
|
||||
desc: 'Whether you are seeking AI solutions or a like-minded partner, we look forward to speaking with you.'
|
||||
}
|
||||
},
|
||||
newsView: {
|
||||
badge: 'COMPANY NEWS',
|
||||
title: 'Corporate News',
|
||||
intro: 'Stay up to date with Open Computing AI, explore the latest AI industry insights, and witness the intelligent leap with us.',
|
||||
detail: 'Details',
|
||||
all: 'All',
|
||||
loadFail: 'Failed to load news list',
|
||||
categories: {
|
||||
corporate: 'Corporate News',
|
||||
product: 'Product News',
|
||||
industry: 'Industry Insights',
|
||||
event: 'Event News'
|
||||
}
|
||||
},
|
||||
casePages: {
|
||||
decision: {
|
||||
heroTitle: 'Investment Strategy Agent',
|
||||
heroDesc: 'An AI-powered assistant designed to support enterprise investment decision-making. It integrates multi-dimensional market data, industry trends, and policy information, and simulates returns and risks across different decision scenarios through algorithms, automatically generating visual analysis reports. It helps enterprises quickly synthesize core information, anticipate market directions, reduce decision-making bias, and enhance the scientific rigor and feasibility of investment proposals.',
|
||||
tabs: { scenarios: 'Use Cases', solution: 'Solutions', highlights: 'Key Highlights' },
|
||||
scenariosTitle: 'Application Scenarios',
|
||||
scenariosDesc: 'Covering the full lifecycle of enterprise investment decision-making',
|
||||
scenarios: [
|
||||
{ title: 'Investment Feasibility Study', desc: 'Collects industry data, market trends, and policy information and generates professional investment feasibility analysis reports, helping decision-makers rapidly evaluate project value and risks.' },
|
||||
{ title: 'Market Research & Analysis', desc: 'Integrates multi-dimensional market data, automatically analyzes market size, competitive landscape, and development trends, and generates detailed market research reports to support business decisions.' },
|
||||
{ title: 'Competitive Product Analysis', desc: 'Automatically collects competitive product information, analyzes product features, market strategies, and user feedback, and generates competitive comparison reports to help enterprises develop differentiated competitive strategies.' }
|
||||
],
|
||||
solutionTitle: 'Challenges & Solutions for Enterprise Decision-Making',
|
||||
solutionDesc: 'Focusing on data collection, research writing, knowledge management, and intelligent applications',
|
||||
challengeTitle: 'Challenge: Data-Rich, Insight-Poor',
|
||||
challenges: [
|
||||
{ title: 'Severe Data Silos', desc: 'Data is scattered across different business systems or website sources, making it difficult to integrate, which impedes comprehensive analysis and decision-making.' },
|
||||
{ title: 'Difficulty in Knowledge Accumulation', desc: "Key business knowledge and expert experience primarily reside in individuals' minds and are lost with personnel turnover." },
|
||||
{ title: 'Time-Consuming and Labor-Intensive Report Writing', desc: 'Manually organizing data and producing high-quality decision reports is cumbersome and inefficient, making it difficult to respond quickly to demands.' }
|
||||
],
|
||||
abilityTitle: 'Solutions: Core Capabilities of the E-Decision Platform',
|
||||
abilityDesc: 'Focusing on four dimensions - data collection, research writing, knowledge management, and intelligent applications - to help enterprises build proprietary knowledge systems and empower scientific decision-making.',
|
||||
abilities: [
|
||||
{ title: 'Data Collection & Integration', desc: 'Automatically captures internal and external data sources, breaks down barriers, and unifies data standards.' },
|
||||
{ title: 'Knowledge Management & Accumulation', desc: 'Transforms unstructured documents into structured knowledge graphs, accumulating core enterprise assets.' },
|
||||
{ title: 'Intelligent Applications & Q&A', desc: 'Supports natural language queries for rapid retrieval and responses, delivering key insights on demand.' },
|
||||
{ title: 'AI-Assisted Research Report Writing', desc: 'Accepts a topic input to automatically generate content, with one-click export to PDF/PPT.' }
|
||||
],
|
||||
highlightsTitle: 'Project Highlights',
|
||||
highlightsDesc: 'One Research, Dual-Format Delivery',
|
||||
highlights: [
|
||||
{ title: 'One-Click PDF Research Report Generation', desc: 'After AI-assisted generation of research content, the system supports one-click export to a well-formatted, detailed PDF research report, meeting archiving and in-depth reading needs.' },
|
||||
{ title: 'One-Click PPT Presentation Generation', desc: 'In addition to PDF, the system supports one-click export to PPT presentations with automatic logical structuring, intelligent pagination, and professional formatting - ready to use immediately.' }
|
||||
],
|
||||
ctaTitle: 'Start Using the Investment Strategy Agent',
|
||||
ctaDesc: 'Let AI empower your investment decisions with one research workflow and dual-format delivery.',
|
||||
contactSales: 'Contact Sales'
|
||||
},
|
||||
contract: {
|
||||
heroTitle: 'Smart Contract Review',
|
||||
heroDesc: 'A professional review agent designed for the entire contract signing process of enterprise legal and business teams. It aggregates multi-dimensional information including regulatory provisions, internal corporate risk control standards, and historical contract cases. Leveraging the deep semantic parsing capabilities of large language models, it automatically identifies legal risks in contracts and intelligently generates standardized modification suggestions along with clear risk explanations.',
|
||||
tabs: { scenarios: 'Use Cases', solution: 'Solutions', highlights: 'Key Highlights' },
|
||||
scenariosTitle: 'Application Scenarios',
|
||||
scenariosDesc: 'Covering the full lifecycle of the contract review process',
|
||||
scenarios: [
|
||||
{ title: 'Business Contract Preliminary Review', desc: 'Automatically parse general business contracts including sales, services, leasing, and cooperation agreements. Screen risk points against enterprise risk control red lines on a clause-by-clause basis, generate preliminary review opinions, reduce basic legal review workload, and accelerate business pre-approval processes.' },
|
||||
{ title: 'In-depth Risk Control for Complex Commercial Contracts', desc: 'For high-risk specialized contracts involving investment & financing, intellectual property, engineering, confidentiality and non-compete, conduct multi-level risk simulations integrated with complete legal provisions and judicial precedents. Identify deep-seated issues such as authority loopholes, breach defects, and jurisdictional disputes, and generate comprehensive risk assessment documents.' },
|
||||
{ title: 'Multi-party Contract Version Comparison & Revision', desc: 'Automatically identify differences across multiple revision rounds between both parties, distinguish newly added, deleted, and modified clauses, highlight risk-related changes, and simultaneously generate version comparison ledgers to support business negotiations and legal review, avoiding overlooked key risks during revisions.' }
|
||||
],
|
||||
deleted: 'Deleted',
|
||||
added: 'Added',
|
||||
solutionTitle: 'Challenges & Solutions in Enterprise Contract Management',
|
||||
solutionDesc: 'Across four core dimensions: data aggregation, contract review, knowledge accumulation, and intelligent consultation',
|
||||
challengeTitle: 'Current Challenges',
|
||||
challenges: [
|
||||
{ title: 'Fragmented and Dispersed Document Information', desc: 'Contracts, regulations, historical precedents, and internal templates are scattered across different storage channels with no unified search entry, requiring cross-document repeated verification during review.' },
|
||||
{ title: 'Risk Control Experience Difficult to Retain and Reuse', desc: "Senior legal counsel's review points, negotiation baselines, and risk handling experience reside only with individuals, which often leads to inconsistent standards after personnel handover." },
|
||||
{ title: 'Time-Consuming Manual Review with Weak Standardization', desc: 'Manual clause-by-clause review of massive contracts consumes significant manpower, while review standards vary among legal staff, resulting in slow responses for urgent signing scenarios.' }
|
||||
],
|
||||
abilityTitle: 'Solutions',
|
||||
abilities: [
|
||||
{ title: 'Unified Aggregation & Parsing of Multi-source Documents', desc: 'Batch upload PDF, Word, and scanned contracts; OCR recognizes text and image content, and uniformly extracts key information including transaction parties, pricing, performance, and breach clauses in structured format.' },
|
||||
{ title: 'AI-powered Full-Clause Intelligent Review & Correction', desc: 'Layer-by-layer scan of all contract clauses, marking high, medium, and low risks against laws, regulations, and enterprise internal control rules, with standardized compliance replacement clauses provided.' },
|
||||
{ title: 'Legal Knowledge Base Accumulation & Management', desc: 'Transform enterprise standard templates, review checklists, precedents, and internal risk control policies into a structured knowledge base, accumulating enterprise-specific legal assets.' },
|
||||
{ title: 'Natural Language Legal Intelligent Q&A', desc: 'Enables business and legal staff to query compliance requirements, clause drafting, and risk consequences using natural language questions, with instant professional answers and reference clauses.' }
|
||||
],
|
||||
highlightsTitle: 'Project Highlights',
|
||||
highlightsDesc: 'Full-Process Intelligent Risk Control with Four Core Review Capabilities',
|
||||
highlights: [
|
||||
{ title: 'Auto-tagged Risk Levels with Compliance Remediation Clauses', desc: 'Classify risks into three levels: critical, general, and advisory. Precisely locate the corresponding original text paragraphs for each risk, and match replacement clauses aligned with enterprise policies and current laws for each risk item.' },
|
||||
{ title: 'Intelligent Multi-version Contract Comparison & Traceability', desc: 'Upload new and old contracts along with exchanged drafts between both parties; the system automatically highlights added, deleted, and modified clauses, and generates a traceable comparison ledger.' },
|
||||
{ title: 'All-Format Contract Recognition with Precise Scanned Document Parsing', desc: 'Supports uploading of Word, PDF, scanned images, and stamped document files with built-in high-precision OCR, covering both existing and newly added contract documents.' },
|
||||
{ title: 'One-Click Reuse of Enterprise Legal Knowledge for Unified Review Standards', desc: 'Built-in enterprise-specific contract template library, risk control red-line checklist, and industry precedent database; new employees can directly adopt mature review standards.' }
|
||||
]
|
||||
}
|
||||
},
|
||||
contactSales: {
|
||||
title: 'Contact Sales',
|
||||
nameLabel: '1. Please enter your name',
|
||||
namePlaceholder: 'Please enter your name',
|
||||
phoneLabel: '2. Please enter your contact information',
|
||||
phonePlaceholder: 'Please enter your phone number',
|
||||
emailLabel: '3. Please enter your email',
|
||||
emailPlaceholder: 'Please enter your email',
|
||||
companyLabel: '4. Please enter your company name',
|
||||
companyPlaceholder: 'Please enter your company name',
|
||||
enterpriseLabel: '5. Please select your enterprise type (single choice)',
|
||||
provinceLabel: '6. Please select your province (single choice)',
|
||||
provincePlaceholder: 'Enter province name or pinyin initials to search...',
|
||||
messageLabel: '7. If you have other questions, please leave a message',
|
||||
messagePlaceholder: 'Please enter your inquiry...',
|
||||
privacy: 'The information you provide will only be used for this business communication. The company will strictly implement information security protection mechanisms and will not disclose or misuse any of your personal data.',
|
||||
phoneInvalid: 'Please enter a valid phone number',
|
||||
emailInvalid: 'Please enter a valid email address',
|
||||
enterpriseRequired: 'Please select your enterprise type',
|
||||
provinceRequired: 'Please select your province',
|
||||
privacyRequired: 'Please agree to the privacy notice before submitting.',
|
||||
formInvalid: 'Please complete the form.',
|
||||
cancel: 'Cancel',
|
||||
submit: 'Submit Inquiry',
|
||||
qrcode: 'Scan QR code to add official customer service',
|
||||
enterpriseOptions: {
|
||||
largeEnterprise: 'Large Enterprise',
|
||||
sme: 'SME',
|
||||
opcIndividual: 'OPC / Individual',
|
||||
university: 'Academy / Institute',
|
||||
government: 'Government',
|
||||
other: 'Other'
|
||||
}
|
||||
},
|
||||
yunbao: {
|
||||
name: 'Yunbao Assistant',
|
||||
online: 'Online',
|
||||
welcome: "Hi~ I'm Yunbao 👋<br>Would you like to just browse, or chat about your needs directly?",
|
||||
browse: 'Just Browse',
|
||||
direct: 'Chat About Needs',
|
||||
recommend: 'Sure~ Here are our recommended hot products 👇',
|
||||
moreProducts: "Or are there other products you'd like to learn about?",
|
||||
askProduct: 'Which product or service would you like to learn about?',
|
||||
learnMore: 'Learn More →',
|
||||
submit: 'Submit Inquiry',
|
||||
consultantNote: 'Please leave your contact information, and our consultant will reach out to you soon',
|
||||
infoNote: 'Your information will only be used for consultant contact',
|
||||
products: {
|
||||
bid: 'AI Bidding Platform',
|
||||
tender: 'AI Tendering Platform',
|
||||
evaluation: 'AI Evaluation Platform',
|
||||
loggingReview: 'Logging Review',
|
||||
gasMonitor: 'Gas Turbine Smart Monitoring',
|
||||
powerPrice: 'Power Price Forecasting',
|
||||
comic: 'AI Comic Drama Production',
|
||||
contract: 'Smart Contract Review',
|
||||
decision: 'Investment Strategy Agent'
|
||||
},
|
||||
productDescs: {
|
||||
bid: 'Smarter bid document writing engine',
|
||||
contract: 'AI-driven contract risk review, 60%+ efficiency boost',
|
||||
decision: 'Minute-level investment research analysis, precise decision support'
|
||||
},
|
||||
productIntroDefault: 'This is an excellent AI product. Feel free to learn more.',
|
||||
nameRequired: 'Please enter your name and phone number first.',
|
||||
phoneInvalid: 'Please enter a valid 11-digit phone number.',
|
||||
submitted: 'Contact information submitted',
|
||||
consultingContent: 'I would like to inquire about {product}.',
|
||||
defaultContent: 'Yunbao chat consultation',
|
||||
submitting: 'Submitting...',
|
||||
success: 'Received! Our consultant will contact you soon.<br>Thank you for your trust!',
|
||||
submitFail: 'Submission failed. Please try again later.',
|
||||
networkFail: 'Network error. Submission failed. Please try again.',
|
||||
retry: 'Retry',
|
||||
openedContract: 'The Smart Contract Review page has been opened.<br>Anything else you would like to know?',
|
||||
openedDecision: 'The Investment Strategy Agent page has been opened.<br>Anything else you would like to know?',
|
||||
openedPage: 'The related page has been opened. Anything else you would like to know?'
|
||||
},
|
||||
...autoMessages
|
||||
}
|
||||
|
||||
@ -33,6 +33,10 @@ export default {
|
||||
viewMore: '查看更多 →',
|
||||
footerProducts: '产品服务',
|
||||
footerContact: '联系我们',
|
||||
footerAddress: '地址',
|
||||
footerEmail: '邮箱',
|
||||
footerTel: '电话',
|
||||
followOfficialAccount: '关注公众号',
|
||||
onlineChat: '在线咨询',
|
||||
solutionCards: {
|
||||
collaboration: {
|
||||
@ -160,5 +164,242 @@ export default {
|
||||
secondDesc: '凭借 AI 智能体工厂的工业级交付能力与央国企标杆案例,开元云入选福布斯中国人工智能商业落地示范企业。'
|
||||
}
|
||||
},
|
||||
about: {
|
||||
intro: {
|
||||
kicker: 'About Us',
|
||||
title: '关于我们',
|
||||
copyHtml: '开元云科技(Open Computing AI)以<strong class="copy-strong">"让AI无处不在,让智能如此简单"</strong>为使命,通过融合技术赋能千行百业,致力于成为全球领先的AI智能体服务运营商。公司聚焦<strong class="copy-strong">教育科研、工程交通、港口水务、能源电力、生物医药</strong>等战略应用场景,构建全栈AI解决方案,推动产业智能化升级。作为专精特新企业,国家级高新技术企业、科技型中小企业、创新型中小企业和<span class="copy-nowrap">ISO 9001/27001/28000</span>认证单位,2026年入选福布斯中国"AI商业落地示范企业"。与北京大学科创园、中关村NCMatch、浪潮云洲、中国中铁智慧城市研发中心、北港大数据、百度智能云、阿里云等顶尖机构建立深度合作。依托自主研发的AI智能体工厂,实现AI模型服务与商业价值的双重突破。开元云将持续深耕垂直领域AI场景化落地,加速实现战略愿景。'
|
||||
},
|
||||
advantages: {
|
||||
kicker: 'Why Choose Us',
|
||||
title: '为什么选择开元云',
|
||||
fast: {
|
||||
title: '更快',
|
||||
subtitle: '极速推理 · 算力提效',
|
||||
tag: 'AI Infra',
|
||||
desc: '翻倍级拉升模型推理速度,为企业提效提供坚实算力基础设施'
|
||||
},
|
||||
accurate: {
|
||||
title: '更准',
|
||||
subtitle: '垂类适配 · 去幻求真',
|
||||
tag: 'AI Agent',
|
||||
desc: '针对垂类行业场景开展模型预训练、专属知识库与本体论服务'
|
||||
},
|
||||
strong: {
|
||||
title: '更强',
|
||||
subtitle: '全栈能力 · 人机协同',
|
||||
tag: 'AI Ability',
|
||||
desc: '全球领先AI能力模型下沉落地,AI掌控力持续增强'
|
||||
},
|
||||
stable: {
|
||||
title: '更稳',
|
||||
subtitle: '私有部署 · 安全可信',
|
||||
tag: 'AI Safety',
|
||||
desc: '专属私域场景稳定运行大模型与智能体,携手国家级专业团队为您护航'
|
||||
}
|
||||
},
|
||||
credentials: {
|
||||
kicker: 'Credentials',
|
||||
title: '资质与荣誉',
|
||||
highTech: {
|
||||
title: '国家级高新技术企业',
|
||||
desc: '经国家认定的高新技术领域创新型企业'
|
||||
},
|
||||
specialized: {
|
||||
title: '专精特新企业',
|
||||
desc: '专业化、精细化、特色化、新颖化发展方向'
|
||||
},
|
||||
techSme: {
|
||||
title: '科技型中小企业',
|
||||
desc: '具备科技创新能力的中小型企业认定'
|
||||
},
|
||||
iso: {
|
||||
title: 'ISO 9001/27001/28000',
|
||||
desc: '质量管理、信息安全、供应链安全三重认证'
|
||||
},
|
||||
innovative: {
|
||||
title: '创新型中小企业',
|
||||
desc: '具有较高创新能力和发展潜力的企业'
|
||||
},
|
||||
forbes: {
|
||||
title: '福布斯中国',
|
||||
descPrefix: '2026年入选',
|
||||
descHighlight: '"AI商业落地示范企业"',
|
||||
descSuffix: ''
|
||||
}
|
||||
},
|
||||
cta: {
|
||||
title: '携手开元云,共筑智能未来',
|
||||
desc: '无论您是寻求AI解决方案的企业,还是志同道合的合作伙伴,我们期待与您对话'
|
||||
}
|
||||
},
|
||||
newsView: {
|
||||
badge: 'COMPANY NEWS',
|
||||
title: '企业动态',
|
||||
intro: '了解开元云最新动态,把握AI行业前沿资讯,与我们一起见证智能跃迁',
|
||||
detail: '详情',
|
||||
all: '全部',
|
||||
loadFail: '新闻列表加载失败',
|
||||
categories: {
|
||||
corporate: '企业动态',
|
||||
product: '产品动态',
|
||||
industry: '行业洞察',
|
||||
event: '活动资讯'
|
||||
}
|
||||
},
|
||||
casePages: {
|
||||
decision: {
|
||||
heroTitle: '投策智能体',
|
||||
heroDesc: '面向企业投资决策的辅助智能体,整合多维度市场数据、行业趋势与政策信息,通过算法模拟不同决策场景的收益与风险,自动生成可视化分析报告。帮助企业快速梳理核心信息,预判市场走向,辅助降低决策偏差,提升投资方案的科学性与可行性',
|
||||
tabs: { scenarios: '应用场景', solution: '解决方案', highlights: '项目亮点' },
|
||||
scenariosTitle: '应用场景',
|
||||
scenariosDesc: '覆盖企业投资决策全链路',
|
||||
scenarios: [
|
||||
{ title: '投资可行性研究', desc: '自动收集行业数据、市场趋势、政策信息,生成专业的投资可行性分析报告,辅助决策者快速评估项目价值与风险。' },
|
||||
{ title: '市场调研分析', desc: '整合多维度市场数据,自动分析市场规模、竞争格局、发展趋势,生成详实的市场调研报告,支撑业务决策。' },
|
||||
{ title: '竞品分析', desc: '自动收集竞品信息,分析产品特性、市场策略、用户反馈,生成竞品对比分析报告,帮助企业制定差异化竞争策略。' }
|
||||
],
|
||||
solutionTitle: '企业决策的困境与解决方案',
|
||||
solutionDesc: '聚焦数据收集、研报撰写、知识管理、智能应用四大维度',
|
||||
challengeTitle: '困境:数据丰富,洞察贫乏',
|
||||
challenges: [
|
||||
{ title: '数据孤岛严重', desc: '数据分散在不同业务系统中、或者网站信源中,难以打通整合,阻碍全面分析与决策。' },
|
||||
{ title: '知识沉淀难', desc: '关键业务知识与专家经验主要存在于个人头脑中,随人员流动而流失。' },
|
||||
{ title: '报告撰写耗时费力', desc: '手工整理数据、制作高质量决策报告过程繁琐,效率低下,难以快速响应需求。' }
|
||||
],
|
||||
abilityTitle: '解决方案:E决策平台核心能力',
|
||||
abilityDesc: '聚焦“数据收集、研报撰写、知识管理、智能应用”四大维度,帮助企业构建专属知识体系,赋能科学决策。',
|
||||
abilities: [
|
||||
{ title: '数据收集与整合', desc: '自动抓取内外部数据源,打破壁垒,统一数据口径' },
|
||||
{ title: '知识管理与沉淀', desc: '将非结构化文档转化为结构化知识图谱,沉淀企业核心资产' },
|
||||
{ title: '智能应用与问答', desc: '支持自然语言提问,快速检索与回答,即问即得关键洞察' },
|
||||
{ title: 'AI辅助研报撰写', desc: '输入主题自动生成内容,支持一键导出为PDF/PPT' }
|
||||
],
|
||||
highlightsTitle: '项目亮点',
|
||||
highlightsDesc: '一次研究,双格式交付',
|
||||
highlights: [
|
||||
{ title: '一键生成PDF研究报告', desc: '在AI辅助生成研报内容后,系统支持一键导出为格式规范、内容详实的PDF研究报告,满足归档与深度阅读需求。' },
|
||||
{ title: '一键生成PPT演示文稿', desc: '除了PDF,系统还支持一键导出为PPT演示文稿,并自动完成逻辑梳理、智能分页和专业排版,即导即用。' }
|
||||
],
|
||||
ctaTitle: '开始使用投策智能体',
|
||||
ctaDesc: '让AI赋能您的投资决策,一次研究,双格式交付',
|
||||
contactSales: '联系销售'
|
||||
},
|
||||
contract: {
|
||||
heroTitle: '合同智能审查',
|
||||
heroDesc: '面向企业法务与业务签约全流程的专业审查智能体,聚合法规条文、企业内部风控标准、历史合同案例多维信息,依托大模型深度语义解析能力自动识别合同法律隐患,智能生成标准化修改建议与风险说明。',
|
||||
tabs: { scenarios: '应用场景', solution: '解决方案', highlights: '项目亮点' },
|
||||
scenariosTitle: '应用场景',
|
||||
scenariosDesc: '覆盖企业合同全生命周期审核链路',
|
||||
scenarios: [
|
||||
{ title: '业务合同初审', desc: '自动解析购销、服务、租赁、合作类通用业务合同,逐条对标企业风控红线快速筛查风险点,输出初审意见,减轻法务基础审核工作量,快速完成业务前置审批。' },
|
||||
{ title: '复杂商事合同深度风控', desc: '针对投融资、知识产权、工程、保密竞业等高风险专项合同,联动完整法条与司法判例开展多层级风险推演,梳理权责漏洞、违约缺陷、管辖争议等深层隐患,输出完整风险评估文档。' },
|
||||
{ title: '多方合同版本比对修订', desc: '自动识别甲乙双方多轮修改稿件差异,区分新增、删减、修改条款,高亮标注风险变更内容,同步生成版本对比台账,辅助商务谈判与法务复核,避免改稿遗漏关键风险。' }
|
||||
],
|
||||
deleted: '删除',
|
||||
added: '新增',
|
||||
solutionTitle: '企业合同管理的困境与解决方案',
|
||||
solutionDesc: '围绕数据归集、合同编审、知识沉淀、智能咨询四大核心维度',
|
||||
challengeTitle: '现存困境',
|
||||
challenges: [
|
||||
{ title: '文件信息割裂分散', desc: '合同、法规、历史判例、内部模板散落在不同存储渠道,无统一检索入口,审核时需跨文件反复核对。' },
|
||||
{ title: '风控经验难以留存复用', desc: '资深法务的审查要点、谈判底线、风险处置经验仅存于个人,人员交接后容易出现标准不统一。' },
|
||||
{ title: '人工审核耗时长、标准化弱', desc: '海量合同逐条人工审阅消耗大量人力,不同法务审核尺度参差不齐,紧急签约场景响应慢。' }
|
||||
],
|
||||
abilityTitle: '解决方案',
|
||||
abilities: [
|
||||
{ title: '多源文件统一归集解析', desc: '批量上传PDF、Word、扫描件合同,OCR识别图文内容,统一结构化抽取交易主体、价款、履约、违约等关键信息。' },
|
||||
{ title: '全条款AI智能编审修正', desc: '分层扫描合同全部条款,对标法律法规与企业内控规则标记高中低三级风险,配套标准化合规替换条款。' },
|
||||
{ title: '法务知识库沉淀管理', desc: '将企业标准模板、审查清单、判例、内部风控制度转化为结构化知识库,沉淀企业专属法务资产。' },
|
||||
{ title: '自然语言法务智能问答', desc: '支持业务、法务人员以口语化提问查询合规要求、条款写法、风险后果,即时输出专业解答与参考条款。' }
|
||||
],
|
||||
highlightsTitle: '项目亮点',
|
||||
highlightsDesc: '全流程智能风控,四大核心审查能力落地',
|
||||
highlights: [
|
||||
{ title: '分级风险自动标注,配套合规整改条款', desc: '按重大、一般、提示三级划分风险,精准定位风险对应原文段落,每条风险同步匹配适配企业制度与现行法律的替换条款。' },
|
||||
{ title: '多版本合同差异智能比对溯源', desc: '上传新旧合同、甲乙双方往来稿件,系统自动高亮新增、删减、变更条款,生成可追溯对比台账。' },
|
||||
{ title: '全格式合同兼容识别,扫描件精准解析', desc: '支持Word、PDF、图片扫描件、盖章影像文件上传,内置高精度OCR图文识别,覆盖存量和增量合同文件。' },
|
||||
{ title: '企业法务知识一键复用,审查标准统一', desc: '内置企业专属合同模板库、风控红线清单、行业判例库,新员工可直接复用成熟审查标准。' }
|
||||
]
|
||||
}
|
||||
},
|
||||
contactSales: {
|
||||
title: '联系销售',
|
||||
nameLabel: '1. 请填写您的姓名',
|
||||
namePlaceholder: '请输入您的姓名',
|
||||
phoneLabel: '2. 请填写您的联系方式',
|
||||
phonePlaceholder: '请输入您的联系电话',
|
||||
emailLabel: '3. 请填写您的邮箱',
|
||||
emailPlaceholder: '请输入您的邮箱',
|
||||
companyLabel: '4. 请填写您的公司名称',
|
||||
companyPlaceholder: '请输入您的公司名称',
|
||||
enterpriseLabel: '5. 请选择您的企业类型(单选)',
|
||||
provinceLabel: '6. 请选择您所在的省份(单选)',
|
||||
provincePlaceholder: '输入省份名称或拼音首字母搜索...',
|
||||
messageLabel: '7. 如果您有其他问题需要咨询,请留言',
|
||||
messagePlaceholder: '请输入您想咨询的内容...',
|
||||
privacy: '您填写的信息仅用于本次业务对接沟通,公司将严格落实信息安全保护机制,不泄露、不滥用您的任何个人资料。',
|
||||
phoneInvalid: '请输入正确的手机号',
|
||||
emailInvalid: '请输入正确的邮箱地址',
|
||||
enterpriseRequired: '请选择企业类型',
|
||||
provinceRequired: '请选择所在省份',
|
||||
privacyRequired: '请勾选同意协议后再提交!',
|
||||
formInvalid: '请完善表单信息~',
|
||||
cancel: '取消',
|
||||
submit: '提交咨询',
|
||||
qrcode: '扫码添加官方客服',
|
||||
enterpriseOptions: {
|
||||
largeEnterprise: '大型企业',
|
||||
sme: '中小企业',
|
||||
opcIndividual: 'OPC个人',
|
||||
university: '高校科研机构',
|
||||
government: '政府',
|
||||
other: '其他'
|
||||
}
|
||||
},
|
||||
yunbao: {
|
||||
name: '云宝小助手',
|
||||
online: '在线',
|
||||
welcome: '嗨~ 我是云宝 👋<br>请问您想随便看看,还是直接聊聊需求?',
|
||||
browse: '随便看看',
|
||||
direct: '直接聊聊需求',
|
||||
recommend: '好的~为您推荐我们的热门产品 👇',
|
||||
moreProducts: '或者您还有其他想了解的产品吗?',
|
||||
askProduct: '您想了解哪方面的产品或服务呢?',
|
||||
learnMore: '查看更多→',
|
||||
submit: '提交咨询',
|
||||
consultantNote: '请留下您的联系方式,我们的顾问将尽快与您联系 😊',
|
||||
infoNote: '信息仅用于顾问联系',
|
||||
products: {
|
||||
bid: 'E投标',
|
||||
tender: 'E招标',
|
||||
evaluation: 'E评标',
|
||||
loggingReview: '采伐智审',
|
||||
gasMonitor: '燃机智慧监盘',
|
||||
powerPrice: '电价预测',
|
||||
comic: 'AI漫剧制作',
|
||||
contract: '合同智能审查',
|
||||
decision: '投策智能体'
|
||||
},
|
||||
productDescs: {
|
||||
bid: '更智能的标书写作引擎',
|
||||
contract: 'AI驱动合同风险审查,效率提升60%+',
|
||||
decision: '分钟级投研分析,精准辅助投资决策'
|
||||
},
|
||||
productIntroDefault: '这是一款优秀的AI产品,欢迎进一步了解!',
|
||||
nameRequired: '请先填写姓名和联系电话哦~',
|
||||
phoneInvalid: '请输入正确的 11 位手机号哦~',
|
||||
submitted: '已提交联系方式',
|
||||
consultingContent: '我想咨询关于【{product}】的产品信息',
|
||||
defaultContent: '云宝对话咨询',
|
||||
submitting: '提交中...',
|
||||
success: '收到!我们的顾问将尽快与您联系 🎉<br>感谢您的信任!',
|
||||
submitFail: '提交失败了,您可以稍后重试。',
|
||||
networkFail: '网络开小差了,提交失败,您可以重试一次。',
|
||||
retry: '重新提交',
|
||||
openedContract: '已为您打开合同智能审查页面 📄<br>还有其他想了解的吗?',
|
||||
openedDecision: '已为您打开投策智能体页面 📊<br>还有其他想了解的吗?',
|
||||
openedPage: '已为您打开相关页面。还有其他想了解的吗?'
|
||||
},
|
||||
...autoMessages
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import {getHomePath} from "@/views/setting/tools";
|
||||
|
||||
NProgress.configure({showSpinner: false}); // NProgress Configuration
|
||||
|
||||
const whiteList = ["product", "/tokenMarket", "/modelDetail", "/modelApiDocument", "/login", "/homePage", "/registrationPage", "/shoppingCart", "/homePageImage","/h5HomePage",'/H5about','/modelProductDetail','/ncmatchHome']; // no redirect whitelist
|
||||
const whiteList = ["product", "/tokenMarket", "/modelDetail", "/modelApiDocument", "/login", "/homePage", "/registrationPage", "/shoppingCart", "/homePageImage","/h5HomePage",'/H5about','/modelProductDetail','/ncmatchHome', '/agreement']; // no redirect whitelist
|
||||
|
||||
// 获取用户代理字符串
|
||||
const userAgent = window.navigator.userAgent;
|
||||
@ -80,9 +80,9 @@ router.beforeEach(async (to, from, next) => {
|
||||
// 获取当前域名
|
||||
const hostname = window.location.hostname || '';
|
||||
|
||||
// 特殊处理:在 ncmatch.cn 域名下访问 /homePage/index 时重定向
|
||||
if (hostname.includes('ncmatch.cn') && to.path === '/homePage/index') {
|
||||
console.log("在 ncmatch.cn 域名下访问 /homePage/index,重定向到 /ncmatchHome/index");
|
||||
// 特殊处理:在 ncmatch.cn / zgcopc.opencomputing.cn域名下访问 /homePage/index 时重定向
|
||||
if ((hostname.includes('ncmatch.cn') || hostname.includes('zgcopc.opencomputing.cn')) && to.path === '/homePage/index') {
|
||||
console.log("在 ncmatch.cn / zgcopc.opencomputing.cn 域名下访问 /homePage/index,重定向到 /ncmatchHome/index");
|
||||
next('/ncmatchHome/index');
|
||||
NProgress.done();
|
||||
return;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,7 @@ const COMMON_ROUTE_PATHS = ['/product', '/tokenManagement', '/tokenUsage', '/mod
|
||||
const OPERATION_EXTRA_ROUTE_PATHS = ['/modelManagement', '/modelInfoConfig', '/operationReport', '/newsLog'];
|
||||
|
||||
// 财务角色需要额外补出来的菜单。
|
||||
const FINANCE_EXTRA_ROUTE_PATHS = ['/financialOverview'];
|
||||
const FINANCE_EXTRA_ROUTE_PATHS = ['/financialOverview', '/financialSettlementCenter'];
|
||||
|
||||
const ADMINISTRATOR_ROUTE_FULL_PATH = '/superAdministrator/roleManagement';
|
||||
|
||||
@ -370,7 +370,8 @@ function getAdministratorRoutes(routes, deviceType = 'pc') {
|
||||
}
|
||||
|
||||
function addAdministratorRoutes(accessedRoutes, routes, userRoles = [], deviceType = 'pc') {
|
||||
if (!userRoles.includes(ADMINISTRATOR_ROLE)) {
|
||||
const orgType = parseInt(sessionStorage.getItem('org_type') || sessionStorage.getItem('orgType') || '0')
|
||||
if (!userRoles.includes(ADMINISTRATOR_ROLE) || isCustomer(userRoles) || orgType === 2 || orgType === 3) {
|
||||
return accessedRoutes;
|
||||
}
|
||||
|
||||
@ -522,7 +523,7 @@ const actions = {
|
||||
console.log("用户类型:", userType, "orgType:", orgType, "设备类型:", deviceType);
|
||||
console.log("ACTION generateRoutes - auths:", auths);
|
||||
|
||||
if (!isSuperAdmin && userRoles.includes(ADMINISTRATOR_ROLE)) {
|
||||
if (!isSuperAdmin && userRoles.includes(ADMINISTRATOR_ROLE) && !isCustomer(userRoles) && orgType !== 2 && orgType !== 3) {
|
||||
const administratorRoutes = getAdministratorRoutes(asyncRoutes, deviceType);
|
||||
commit("SET_ROUTES", administratorRoutes);
|
||||
resolve(administratorRoutes);
|
||||
|
||||
84
f/web-kboss/src/utils/consultDict.js
Normal file
84
f/web-kboss/src/utils/consultDict.js
Normal file
@ -0,0 +1,84 @@
|
||||
export function isEnglishLocale(i18n) {
|
||||
return !!(i18n && i18n.locale === 'en-US')
|
||||
}
|
||||
|
||||
export function getDictLabel(item, isEn) {
|
||||
if (!item) return ''
|
||||
if (isEn) {
|
||||
return item.dict_value_en || item.dict_value_zh || item.dict_value || ''
|
||||
}
|
||||
return item.dict_value_zh || item.dict_value || item.dict_value_en || ''
|
||||
}
|
||||
|
||||
export function extractConsultOptionList(res) {
|
||||
if (res && Array.isArray(res.data)) return res.data
|
||||
if (res && res.data && Array.isArray(res.data.data)) return res.data.data
|
||||
return []
|
||||
}
|
||||
|
||||
export function normalizeDictOptions(list, type) {
|
||||
return list
|
||||
.filter(item => item && item.dict_type === type)
|
||||
.sort((a, b) => Number(a.sort_order || 0) - Number(b.sort_order || 0))
|
||||
.map(item => ({
|
||||
id: String(item.dict_key),
|
||||
nameZh: item.dict_value_zh || item.dict_value || '',
|
||||
nameEn: item.dict_value_en || item.dict_value_zh || item.dict_value || ''
|
||||
}))
|
||||
}
|
||||
|
||||
export function parseDirectionParts(label, isEn) {
|
||||
if (!label) {
|
||||
return { title: '', desc: '' }
|
||||
}
|
||||
|
||||
const text = String(label)
|
||||
if (isEn) {
|
||||
const match = text.match(/^(.+?)\s*\((.+)\)\s*$/)
|
||||
return match
|
||||
? { title: match[1].trim(), desc: match[2].trim() }
|
||||
: { title: text, desc: '' }
|
||||
}
|
||||
|
||||
const match = text.match(/^(.+?)((.+?))$/)
|
||||
return match
|
||||
? { title: match[1].trim(), desc: match[2].trim() }
|
||||
: { title: text, desc: '' }
|
||||
}
|
||||
|
||||
export function normalizeDirectionOptions(list) {
|
||||
return list
|
||||
.filter(item => item && item.dict_type === 'direction')
|
||||
.sort((a, b) => Number(a.sort_order || 0) - Number(b.sort_order || 0))
|
||||
.map(item => {
|
||||
const nameZh = item.dict_value_zh || item.dict_value || ''
|
||||
const nameEn = item.dict_value_en || nameZh
|
||||
const parts = parseDirectionParts(nameZh, false)
|
||||
|
||||
return {
|
||||
id: String(item.dict_key),
|
||||
nameZh,
|
||||
nameEn,
|
||||
titleZh: parts.title,
|
||||
descZh: parts.desc,
|
||||
titleEn: parseDirectionParts(nameEn, true).title,
|
||||
descEn: parseDirectionParts(nameEn, true).desc
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function localizeDictOptions(options, isEn) {
|
||||
return (options || []).map(item => ({
|
||||
...item,
|
||||
name: isEn ? item.nameEn : item.nameZh
|
||||
}))
|
||||
}
|
||||
|
||||
export function localizeDirectionOptions(options, isEn) {
|
||||
return (options || []).map(item => ({
|
||||
...item,
|
||||
name: isEn ? item.nameEn : item.nameZh,
|
||||
title: isEn ? item.titleEn : item.titleZh,
|
||||
desc: isEn ? item.descEn : item.descZh
|
||||
}))
|
||||
}
|
||||
@ -14,7 +14,7 @@ export default function getPageTitle(pageTitle) {
|
||||
return `开元云(北京)科技有限公司`
|
||||
}
|
||||
if (domainName.indexOf('ncmatch') > -1) {
|
||||
return `NCMatch`
|
||||
return `数智开物`
|
||||
}
|
||||
return `opencomputing`
|
||||
}
|
||||
|
||||
@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<SettlementPageFrame>
|
||||
<SettlementPageHeader
|
||||
icon="el-icon-circle-check"
|
||||
title="审批回调"
|
||||
description="处理审批结果。审批通过后自动结算记账,供应商调用 SettleAccounting,分销商写入账单和账本。"
|
||||
endpoint="GET /bill/finance_settlement_apv_callback.dspy"
|
||||
>
|
||||
<template #actions>
|
||||
<button type="button" class="settlement-btn settlement-btn--primary" :disabled="loadingList" @click="loadList">
|
||||
<i class="el-icon-refresh" />{{ loadingList ? '刷新中...' : '刷新' }}
|
||||
</button>
|
||||
</template>
|
||||
</SettlementPageHeader>
|
||||
|
||||
<div class="settlement-split-layout">
|
||||
<aside class="settlement-card settlement-selector">
|
||||
<div class="settlement-card-head">
|
||||
<div class="settlement-card-heading">
|
||||
<i class="el-icon-filter" />
|
||||
<span>审批中的结算单</span>
|
||||
<small class="settlement-card-subtitle">· 2</small>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-for="row in approvalRows"
|
||||
:key="row.id"
|
||||
type="button"
|
||||
class="settlement-selector-item"
|
||||
:class="{ 'is-active': selectedRow && row.id === selectedRow.id }"
|
||||
@click="selectRow(row)"
|
||||
>
|
||||
<span class="settlement-selector-no">{{ row.no }}</span>
|
||||
<span class="settlement-selector-name">{{ row.name }}</span>
|
||||
<span class="settlement-selector-date">{{ row.approvalId }}</span>
|
||||
<span style="display: block; margin-top: 7px;"><SettlementStatusBadge :status="row.status" /></span>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<main style="min-width: 0;">
|
||||
<template v-if="selectedRow">
|
||||
<section class="settlement-card" style="margin-bottom: 20px;">
|
||||
<div class="settlement-detail-head">
|
||||
<div style="display: flex; align-items: flex-start; gap: 12px;">
|
||||
<span class="settlement-page-icon" style="width: 40px; height: 40px; font-size: 19px;"><i class="el-icon-document" /></span>
|
||||
<div>
|
||||
<div class="settlement-detail-no">
|
||||
{{ selectedRow.no }}
|
||||
<SettlementStatusBadge :status="selectedRow.status" />
|
||||
</div>
|
||||
<div class="settlement-detail-meta">
|
||||
<i class="el-icon-office-building" /> {{ selectedRow.name }}
|
||||
<span style="margin: 0 7px; color: #cbd5e1;">|</span>
|
||||
<i class="el-icon-date" /> {{ selectedRow.start }} ~ {{ selectedRow.end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settlement-metric-grid settlement-metric-grid--three" style="margin: 0; padding: 0 20px 20px;">
|
||||
<SettlementMetricCard label="销售金额" :value="selectedRow.sales" icon="el-icon-wallet" />
|
||||
<SettlementMetricCard label="结算金额" :value="selectedRow.settlement" variant="emerald" icon="el-icon-s-order" />
|
||||
<SettlementMetricCard label="平台收入" :value="selectedRow.income" variant="amber" icon="el-icon-s-marketing" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settlement-card" style="margin-bottom: 20px;">
|
||||
<div class="settlement-form-section">
|
||||
<div class="settlement-section-title">
|
||||
<span class="settlement-section-title-icon"><i class="el-icon-s-promotion" /></span>
|
||||
<div>
|
||||
<b>审批回调</b>
|
||||
<span>处理审批结果</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settlement-form-stack">
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">审批 ID *</label>
|
||||
<input v-model.trim="form.apv_id" class="settlement-input" type="text" placeholder="请输入审批实例 ID">
|
||||
<small style="display: block; margin-top: 6px; color: #94a3b8; font-size: 11px;">可传 apv_id 或 approval_id</small>
|
||||
</div>
|
||||
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">审批状态 *</label>
|
||||
<div class="settlement-callback-grid">
|
||||
<button type="button" class="settlement-option-card" :class="{ 'is-active': form.status === 'start' }" @click="form.status = 'start'">
|
||||
<span class="settlement-option-card-head"><i class="el-icon-time" />审批中</span>
|
||||
<p>审批进行中</p>
|
||||
</button>
|
||||
<button type="button" class="settlement-option-card" :class="{ 'is-active': form.status === 'agree' }" @click="form.status = 'agree'">
|
||||
<span class="settlement-option-card-head"><i class="el-icon-circle-check" />审批通过</span>
|
||||
<p>自动结算记账</p>
|
||||
</button>
|
||||
<button type="button" class="settlement-option-card" :class="{ 'is-active': form.status === 'refuse' }" @click="form.status = 'refuse'">
|
||||
<span class="settlement-option-card-head"><i class="el-icon-circle-close" />审批拒绝</span>
|
||||
<p>返回待处理状态</p>
|
||||
</button>
|
||||
<button type="button" class="settlement-option-card" :class="{ 'is-active': form.status === 'terminate' }" @click="form.status = 'terminate'">
|
||||
<span class="settlement-option-card-head"><i class="el-icon-remove-outline" />审批撤销</span>
|
||||
<p>终止当前审批流程</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="settlement-alert" style="margin: 0;">
|
||||
<span class="settlement-alert-icon"><i class="el-icon-circle-check" /></span>
|
||||
<div class="settlement-alert-content">
|
||||
<b>审批通过后执行记账</b>
|
||||
<p>供应商结算将调用 SettleAccounting;分销商结算将写入账单和账本。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="result" class="settlement-alert" style="margin: 0;">
|
||||
<span class="settlement-alert-icon"><i class="el-icon-circle-check" /></span>
|
||||
<div class="settlement-alert-content">
|
||||
<b>回调处理成功</b>
|
||||
<p>结算单 ID:{{ result.id || '-' }},处理状态:<SettlementStatusBadge :status="result.status" /></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button type="button" class="settlement-btn settlement-btn--primary" :disabled="submitting" @click="submitCallback">
|
||||
<i class="el-icon-arrow-right" />{{ submitting ? '处理中...' : '提交回调' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settlement-card">
|
||||
<div class="settlement-card-head">
|
||||
<div class="settlement-card-heading">
|
||||
<i class="el-icon-time" />
|
||||
<span>本次会话回调记录</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="item in history" :key="item.time" class="settlement-history-item">
|
||||
<div class="settlement-history-top">
|
||||
<span class="is-mono is-muted">{{ item.time }}</span>
|
||||
<SettlementStatusBadge :status="item.status" />
|
||||
</div>
|
||||
<div class="settlement-history-meta">
|
||||
<span>结算单:<b>{{ item.no }}</b></span>
|
||||
<span>审批 ID:<b>{{ item.apvId }}</b></span>
|
||||
<span>操作:<b>{{ item.action }}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!history.length" class="settlement-history-item" style="color: #94a3b8;">暂无本次会话回调记录</div>
|
||||
</section>
|
||||
</template>
|
||||
<section v-else class="settlement-card settlement-card--padded" style="color: #94a3b8; text-align: center;">
|
||||
{{ loadingList ? '正在加载审批中的结算单...' : '暂无审批中的结算单' }}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</SettlementPageFrame>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettlementPageFrame from './SettlementPageFrame'
|
||||
import SettlementPageHeader from './SettlementPageHeader'
|
||||
import SettlementMetricCard from './SettlementMetricCard'
|
||||
import SettlementStatusBadge from './SettlementStatusBadge'
|
||||
import { approvalCallbackAPI, settlementListAPI } from '@/api/FinancialSettlementCenter'
|
||||
import {
|
||||
getAccountingOrgId,
|
||||
getItems,
|
||||
getPayload,
|
||||
normalizeSettlement
|
||||
} from './settlementHelpers'
|
||||
|
||||
export default {
|
||||
name: 'ApprovalCallback',
|
||||
components: {
|
||||
SettlementPageFrame,
|
||||
SettlementPageHeader,
|
||||
SettlementMetricCard,
|
||||
SettlementStatusBadge
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loadingList: false,
|
||||
submitting: false,
|
||||
approvalRows: [],
|
||||
selectedRow: null,
|
||||
result: null,
|
||||
history: [],
|
||||
form: {
|
||||
apv_id: '',
|
||||
status: 'agree'
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadList()
|
||||
},
|
||||
methods: {
|
||||
async loadList() {
|
||||
const accountingOrgid = getAccountingOrgId(this.$route)
|
||||
if (!accountingOrgid) {
|
||||
this.$message.warning('未获取到核算机构 ID')
|
||||
return
|
||||
}
|
||||
|
||||
this.loadingList = true
|
||||
try {
|
||||
const payload = getPayload(await settlementListAPI({
|
||||
accounting_orgid: accountingOrgid,
|
||||
current_page: 1,
|
||||
page_size: 100
|
||||
}))
|
||||
this.approvalRows = getItems(payload).map(normalizeSettlement).filter(row => {
|
||||
return row.approvalId && ['approving', 'approved'].includes(row.status)
|
||||
})
|
||||
this.selectedRow = this.approvalRows[0] || null
|
||||
this.form.apv_id = this.selectedRow ? this.selectedRow.approvalId : ''
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '审批中结算单查询失败')
|
||||
} finally {
|
||||
this.loadingList = false
|
||||
}
|
||||
},
|
||||
selectRow(row) {
|
||||
this.selectedRow = row
|
||||
this.form.apv_id = row.approvalId || ''
|
||||
this.result = null
|
||||
},
|
||||
async submitCallback() {
|
||||
if (!this.form.apv_id) {
|
||||
this.$message.warning('请输入审批 ID')
|
||||
return
|
||||
}
|
||||
|
||||
this.submitting = true
|
||||
try {
|
||||
const payload = getPayload(await approvalCallbackAPI({
|
||||
apv_id: this.form.apv_id,
|
||||
status: this.form.status
|
||||
}))
|
||||
const item = Array.isArray(payload) ? payload[0] : (getItems(payload)[0] || payload)
|
||||
this.result = normalizeSettlement(item)
|
||||
this.history.unshift({
|
||||
time: new Date().toLocaleString('zh-CN'),
|
||||
no: this.selectedRow ? this.selectedRow.no : '-',
|
||||
apvId: this.form.apv_id,
|
||||
action: this.getActionLabel(),
|
||||
status: this.result.status
|
||||
})
|
||||
this.$message.success('审批回调处理成功')
|
||||
await this.loadList()
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '审批回调处理失败')
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
},
|
||||
getActionLabel() {
|
||||
return {
|
||||
start: '审批中',
|
||||
agree: '审批通过',
|
||||
refuse: '审批拒绝',
|
||||
terminate: '审批撤销'
|
||||
}[this.form.status]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<SettlementPageFrame>
|
||||
<SettlementPageHeader
|
||||
icon="el-icon-circle-plus-outline"
|
||||
title="创建结算单"
|
||||
description="创建结算单主表和明细快照。创建后状态为草稿(draft),同一账本、对手方和账期不能重复创建。"
|
||||
endpoint="GET /bill/finance_settlement_create.dspy"
|
||||
/>
|
||||
|
||||
<div class="settlement-form-layout">
|
||||
<section class="settlement-card">
|
||||
<div class="settlement-form-section">
|
||||
<div class="settlement-section-title">
|
||||
<span class="settlement-section-title-icon"><i class="el-icon-circle-plus-outline" /></span>
|
||||
<div>
|
||||
<b>结算单信息</b>
|
||||
<span>填写结算单核心信息</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settlement-form-stack">
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">对手方类型 *</label>
|
||||
<div class="settlement-option-grid">
|
||||
<button type="button" class="settlement-option-card" :class="{ 'is-active': form.counterparty_type === 'supplier' }" @click="form.counterparty_type = 'supplier'">
|
||||
<span class="settlement-option-card-head"><i class="el-icon-office-building" />供应商</span>
|
||||
<p>按待结转科目计算结算</p>
|
||||
</button>
|
||||
<button type="button" class="settlement-option-card" :class="{ 'is-active': form.counterparty_type === 'reseller' }" @click="form.counterparty_type = 'reseller'">
|
||||
<span class="settlement-option-card-head"><i class="el-icon-s-shop" />分销商</span>
|
||||
<p>按分销商资金科目计算</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">选择对手方 *</label>
|
||||
<input v-model.trim="form.counterparty_orgid" class="settlement-input" type="text" placeholder="请输入供应商或分销商机构 ID">
|
||||
</div>
|
||||
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">账期类型 *</label>
|
||||
<div class="settlement-option-grid">
|
||||
<button type="button" class="settlement-option-card" :class="{ 'is-active': form.period_type === 'day' }" @click="form.period_type = 'day'">
|
||||
<span class="settlement-option-card-head"><i class="el-icon-date" />日结</span>
|
||||
<p>按单日账期生成结算单</p>
|
||||
</button>
|
||||
<button type="button" class="settlement-option-card" :class="{ 'is-active': form.period_type === 'month' }" @click="form.period_type = 'month'">
|
||||
<span class="settlement-option-card-head"><i class="el-icon-date" />月结</span>
|
||||
<p>按自然月账期生成结算单</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settlement-inline-grid">
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">账期开始 *</label>
|
||||
<input v-model="form.period_start" class="settlement-input" type="date">
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">账期结束 *</label>
|
||||
<input v-model="form.period_end" class="settlement-input" type="date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">操作用户 ID</label>
|
||||
<input v-model.trim="form.userid" class="settlement-input" type="text" placeholder="请输入当前操作用户 ID">
|
||||
</div>
|
||||
|
||||
<div class="settlement-actions" style="padding-top: 3px;">
|
||||
<button type="button" class="settlement-btn settlement-btn--primary" :disabled="creating" @click="createSettlement">
|
||||
<i class="el-icon-circle-plus-outline" />{{ creating ? '创建中...' : '创建结算单' }}
|
||||
</button>
|
||||
<button type="button" class="settlement-btn" @click="goList">
|
||||
查看列表<i class="el-icon-arrow-right" style="margin: 0 0 0 6px;" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside>
|
||||
<section class="settlement-card settlement-card--padded" style="margin-bottom: 18px;">
|
||||
<div class="settlement-card-heading" style="margin-bottom: 16px;">
|
||||
<i class="el-icon-info" />
|
||||
<span>创建规则</span>
|
||||
</div>
|
||||
<ul class="settlement-help-list">
|
||||
<li>仅统计已记账账单(bill_state = '1')</li>
|
||||
<li>供应商按待结转科目且贷方计算</li>
|
||||
<li>分销商按分销商存放资金且借方计算</li>
|
||||
<li>平台收入由折扣收入和底价收入构成</li>
|
||||
<li>创建后状态为 draft,需提交审批</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="settlement-card settlement-card--padded">
|
||||
<div class="settlement-card-heading" style="margin-bottom: 15px;">
|
||||
<i :class="result ? 'el-icon-circle-check' : 'el-icon-view'" />
|
||||
<span>{{ result ? '创建成功' : '本次创建预览' }}</span>
|
||||
</div>
|
||||
<div class="settlement-info-grid" style="grid-template-columns: 1fr; padding: 0;">
|
||||
<div>
|
||||
<div class="settlement-info-label">账本机构</div>
|
||||
<div class="settlement-info-value is-mono">{{ accountingOrgid || '-' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="settlement-info-label">结算对手方</div>
|
||||
<div class="settlement-info-value is-mono">{{ form.counterparty_orgid || '-' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="settlement-info-label">结算账期</div>
|
||||
<div class="settlement-info-value is-mono">{{ form.period_start }} ~ {{ form.period_end }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="settlement-info-label">{{ result ? '结算单号' : '预估状态' }}</div>
|
||||
<div v-if="result" class="settlement-info-value is-mono">{{ result.no || result.id }}</div>
|
||||
<SettlementStatusBadge v-else status="draft" />
|
||||
</div>
|
||||
<div v-if="result">
|
||||
<div class="settlement-info-label">创建状态</div>
|
||||
<SettlementStatusBadge :status="result.status" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</SettlementPageFrame>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettlementPageFrame from './SettlementPageFrame'
|
||||
import SettlementPageHeader from './SettlementPageHeader'
|
||||
import SettlementStatusBadge from './SettlementStatusBadge'
|
||||
import { createSettlementStatementAPI } from '@/api/FinancialSettlementCenter'
|
||||
import {
|
||||
getAccountingOrgId,
|
||||
getCurrentUserId,
|
||||
getPayload,
|
||||
normalizeSettlement
|
||||
} from './settlementHelpers'
|
||||
|
||||
export default {
|
||||
name: 'CreateSettlementStatement',
|
||||
components: {
|
||||
SettlementPageFrame,
|
||||
SettlementPageHeader,
|
||||
SettlementStatusBadge
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
creating: false,
|
||||
result: null,
|
||||
accountingOrgid: getAccountingOrgId(this.$route),
|
||||
form: {
|
||||
counterparty_type: 'supplier',
|
||||
counterparty_orgid: '',
|
||||
period_type: 'month',
|
||||
period_start: '2026-06-01',
|
||||
period_end: '2026-06-30',
|
||||
userid: getCurrentUserId()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async createSettlement() {
|
||||
this.accountingOrgid = getAccountingOrgId(this.$route)
|
||||
if (!this.accountingOrgid) {
|
||||
this.$message.warning('未获取到核算机构 ID')
|
||||
return
|
||||
}
|
||||
if (!this.form.counterparty_orgid || !this.form.userid) {
|
||||
this.$message.warning('请填写对手方机构 ID 和操作用户 ID')
|
||||
return
|
||||
}
|
||||
|
||||
this.creating = true
|
||||
try {
|
||||
const payload = getPayload(await createSettlementStatementAPI({
|
||||
accounting_orgid: this.accountingOrgid,
|
||||
counterparty_type: this.form.counterparty_type,
|
||||
counterparty_orgid: this.form.counterparty_orgid,
|
||||
period_type: this.form.period_type,
|
||||
period_start: this.form.period_start,
|
||||
period_end: this.form.period_end,
|
||||
userid: this.form.userid
|
||||
}))
|
||||
this.result = normalizeSettlement(payload.settlement || payload)
|
||||
this.$message.success('结算单创建成功')
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '创建结算单失败')
|
||||
} finally {
|
||||
this.creating = false
|
||||
}
|
||||
},
|
||||
goList() {
|
||||
this.$router.push({ name: 'FinancialSettlementStatementList' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<SettlementPageFrame>
|
||||
<SettlementPageHeader
|
||||
icon="el-icon-s-marketing"
|
||||
title="平台收入展示"
|
||||
description="展示账期内从供应商和分销商结算中获得的平台收入,包含趋势、占比及对手方贡献排行。"
|
||||
endpoint="GET /bill/finance_settlement_summary.dspy"
|
||||
>
|
||||
<template #actions>
|
||||
<button type="button" class="settlement-btn settlement-btn--primary" :disabled="loading" @click="loadData">
|
||||
<i class="el-icon-refresh" />{{ loading ? '刷新中...' : '刷新' }}
|
||||
</button>
|
||||
</template>
|
||||
</SettlementPageHeader>
|
||||
|
||||
<section class="settlement-card settlement-card--padded" style="margin-bottom: 24px;">
|
||||
<div class="settlement-card-heading" style="margin-bottom: 18px;">
|
||||
<i class="el-icon-date" />
|
||||
<span>统计区间</span>
|
||||
</div>
|
||||
<div class="settlement-filter-grid settlement-filter-grid--three">
|
||||
<div class="settlement-inline-grid">
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">开始日期</label>
|
||||
<input v-model="form.start_date" class="settlement-input" type="date">
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">结束日期</label>
|
||||
<input v-model="form.end_date" class="settlement-input" type="date">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">快捷区间</label>
|
||||
<div class="settlement-actions">
|
||||
<button type="button" class="settlement-btn" @click="setRange('2026-01-01', '2026-12-31')">本年</button>
|
||||
<button type="button" class="settlement-btn" @click="setRange('2026-01-01', '2026-06-30')">近半年</button>
|
||||
<button type="button" class="settlement-btn" @click="setRange('2026-04-01', '2026-06-30')">本季度</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="settlement-metric-grid">
|
||||
<SettlementMetricCard label="平台总收入" :value="money(totalIncome)" hint="折扣收入 + 底价收入" icon="el-icon-wallet" />
|
||||
<SettlementMetricCard label="供应商贡献" :value="money(supplierIncome)" :hint="`占比 ${supplierRatio}`" variant="emerald" icon="el-icon-office-building" />
|
||||
<SettlementMetricCard label="分销商贡献" :value="money(resellerIncome)" :hint="`占比 ${resellerRatio}`" variant="violet" icon="el-icon-office-building" />
|
||||
<SettlementMetricCard label="参与对手方" :value="counterpartyCount" suffix="家" hint="有平台收入贡献" variant="amber" icon="el-icon-user" />
|
||||
</div>
|
||||
|
||||
<div class="settlement-chart-grid">
|
||||
<section class="settlement-card settlement-card--padded">
|
||||
<div class="settlement-card-heading" style="justify-content: space-between; margin-bottom: 8px;">
|
||||
<span><i class="el-icon-s-data" /> 月度平台收入趋势</span>
|
||||
<small class="settlement-card-subtitle">按账期聚合</small>
|
||||
</div>
|
||||
<div class="settlement-bar-chart">
|
||||
<div v-for="item in trendBars" :key="item.period" class="settlement-bar-group">
|
||||
<span class="settlement-bar" :style="{ height: item.supplier }" />
|
||||
<span class="settlement-bar settlement-bar--violet" :style="{ height: item.reseller }" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="settlement-bar-labels">
|
||||
<span v-for="item in trendBars" :key="`${item.period}-label`">{{ item.period }}</span>
|
||||
</div>
|
||||
<div class="settlement-chart-legend">
|
||||
<span>供应商</span>
|
||||
<span>分销商</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settlement-card settlement-card--padded">
|
||||
<div class="settlement-card-heading" style="margin-bottom: 8px;">
|
||||
<i class="el-icon-pie-chart" />
|
||||
<span>收入构成</span>
|
||||
</div>
|
||||
<div class="settlement-donut-layout">
|
||||
<div class="settlement-donut" :style="{ background: donutBackground }">
|
||||
<div class="settlement-donut-value">
|
||||
<small>总计</small>{{ money(totalIncome) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settlement-chart-legend">
|
||||
<span>供应商 {{ supplierRatio }}</span>
|
||||
<span>分销商 {{ resellerRatio }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="settlement-card">
|
||||
<div class="settlement-card-head">
|
||||
<div class="settlement-card-heading">
|
||||
<i class="el-icon-office-building" />
|
||||
<span>对手方贡献排行</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settlement-table-wrap">
|
||||
<table class="settlement-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 72px;">排名</th>
|
||||
<th>对手方名称</th>
|
||||
<th>类型</th>
|
||||
<th class="is-number">平台收入</th>
|
||||
<th class="is-number">占总收入</th>
|
||||
<th>贡献度</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in incomeRanks" :key="row.name">
|
||||
<td><span class="settlement-rank" :class="`settlement-rank--${index + 1}`">{{ index + 1 }}</span></td>
|
||||
<td style="font-weight: 600;">{{ row.name }}</td>
|
||||
<td>
|
||||
<span class="settlement-pill" :class="row.type === '供应商' ? 'settlement-pill--cyan' : 'settlement-pill--violet'">
|
||||
{{ row.type }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="is-number is-amber">{{ row.income }}</td>
|
||||
<td class="is-number is-muted">{{ row.ratio }}</td>
|
||||
<td>
|
||||
<span class="settlement-contribution">
|
||||
<span class="settlement-contribution-bar"><span :style="{ width: row.width }" /></span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!loading && !incomeRanks.length">
|
||||
<td colspan="6" style="padding: 42px; color: #94a3b8; text-align: center;">暂无平台收入数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</SettlementPageFrame>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettlementPageFrame from './SettlementPageFrame'
|
||||
import SettlementPageHeader from './SettlementPageHeader'
|
||||
import SettlementMetricCard from './SettlementMetricCard'
|
||||
import { summaryQueryAPI } from '@/api/FinancialSettlementCenter'
|
||||
import {
|
||||
getAccountingOrgId,
|
||||
getItems,
|
||||
getPayload,
|
||||
getSummary,
|
||||
money,
|
||||
normalizeSummaryItem,
|
||||
numberValue,
|
||||
percent
|
||||
} from './settlementHelpers'
|
||||
|
||||
export default {
|
||||
name: 'PlatformRevenue',
|
||||
components: {
|
||||
SettlementPageFrame,
|
||||
SettlementPageHeader,
|
||||
SettlementMetricCard
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
form: {
|
||||
start_date: '2026-01-01',
|
||||
end_date: '2026-06-30'
|
||||
},
|
||||
supplierItems: [],
|
||||
resellerItems: [],
|
||||
supplierIncome: 0,
|
||||
resellerIncome: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalIncome() {
|
||||
return this.supplierIncome + this.resellerIncome
|
||||
},
|
||||
supplierRatio() {
|
||||
return percent(this.supplierIncome, this.totalIncome)
|
||||
},
|
||||
resellerRatio() {
|
||||
return percent(this.resellerIncome, this.totalIncome)
|
||||
},
|
||||
counterpartyCount() {
|
||||
const names = this.supplierItems.concat(this.resellerItems).map(item => item.id || item.name).filter(Boolean)
|
||||
return Array.from(new Set(names)).length
|
||||
},
|
||||
donutBackground() {
|
||||
const ratio = numberValue(this.totalIncome) ? (this.supplierIncome / this.totalIncome) * 100 : 50
|
||||
return `conic-gradient(#0ea5e9 0 ${ratio}%, #a855f7 ${ratio}% 100%)`
|
||||
},
|
||||
trendBars() {
|
||||
const group = {}
|
||||
this.supplierItems.concat(this.resellerItems).forEach(item => {
|
||||
const period = item.period || '-'
|
||||
if (!group[period]) group[period] = { period, supplier: 0, reseller: 0 }
|
||||
group[period][item.typeKey === 'reseller' ? 'reseller' : 'supplier'] += item.incomeAmount
|
||||
})
|
||||
const items = Object.keys(group).sort().map(key => group[key])
|
||||
const max = Math.max.apply(null, items.map(item => Math.max(item.supplier, item.reseller)).concat([1]))
|
||||
return items.map(item => ({
|
||||
period: item.period,
|
||||
supplier: `${Math.max(5, (item.supplier / max) * 100)}%`,
|
||||
reseller: `${Math.max(5, (item.reseller / max) * 100)}%`
|
||||
}))
|
||||
},
|
||||
incomeRanks() {
|
||||
const groups = {}
|
||||
this.supplierItems.concat(this.resellerItems).forEach(item => {
|
||||
const key = item.id || item.name
|
||||
if (!groups[key]) {
|
||||
groups[key] = {
|
||||
name: item.name || '-',
|
||||
type: item.typeKey === 'reseller' ? '分销商' : '供应商',
|
||||
amount: 0
|
||||
}
|
||||
}
|
||||
groups[key].amount += item.incomeAmount
|
||||
})
|
||||
const rows = Object.keys(groups).map(key => groups[key]).sort((a, b) => b.amount - a.amount)
|
||||
const max = rows.length ? rows[0].amount : 0
|
||||
return rows.slice(0, 10).map(item => ({
|
||||
...item,
|
||||
income: money(item.amount),
|
||||
ratio: percent(item.amount, this.totalIncome),
|
||||
width: max ? `${(item.amount / max) * 100}%` : '0%'
|
||||
}))
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
money,
|
||||
buildParams(counterpartyType) {
|
||||
return {
|
||||
accounting_orgid: getAccountingOrgId(this.$route),
|
||||
counterparty_type: counterpartyType,
|
||||
period_type: 'month',
|
||||
start_date: this.form.start_date,
|
||||
end_date: this.form.end_date,
|
||||
counterparty_orgid: '',
|
||||
current_page: 1,
|
||||
page_size: 1000
|
||||
}
|
||||
},
|
||||
async loadData() {
|
||||
const orgid = getAccountingOrgId(this.$route)
|
||||
if (!orgid) {
|
||||
this.$message.warning('未获取到核算机构 ID')
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const [supplierResponse, resellerResponse] = await Promise.all([
|
||||
summaryQueryAPI(this.buildParams('supplier')),
|
||||
summaryQueryAPI(this.buildParams('reseller'))
|
||||
])
|
||||
const supplierPayload = getPayload(supplierResponse)
|
||||
const resellerPayload = getPayload(resellerResponse)
|
||||
this.supplierItems = getItems(supplierPayload).map(item => ({
|
||||
...normalizeSummaryItem(item),
|
||||
typeKey: 'supplier'
|
||||
}))
|
||||
this.resellerItems = getItems(resellerPayload).map(item => ({
|
||||
...normalizeSummaryItem(item),
|
||||
typeKey: 'reseller'
|
||||
}))
|
||||
this.supplierIncome = getSummary(supplierPayload).incomeAmount || this.supplierItems.reduce((sum, item) => sum + item.incomeAmount, 0)
|
||||
this.resellerIncome = getSummary(resellerPayload).incomeAmount || this.resellerItems.reduce((sum, item) => sum + item.incomeAmount, 0)
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '平台收入查询失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
setRange(startDate, endDate) {
|
||||
this.form.start_date = startDate
|
||||
this.form.end_date = endDate
|
||||
this.loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<SettlementPageFrame>
|
||||
<SettlementPageHeader
|
||||
icon="el-icon-s-operation"
|
||||
title="流程总览"
|
||||
description="财务结算全流程闭环:汇总查询、预览核对、创建结算单、提交审批和审批回调。"
|
||||
endpoint="推荐调用流程"
|
||||
>
|
||||
<template #actions>
|
||||
<button type="button" class="settlement-btn settlement-btn--primary" :disabled="loading" @click="loadData">
|
||||
<i class="el-icon-refresh" />{{ loading ? '刷新中...' : '刷新数据' }}
|
||||
</button>
|
||||
</template>
|
||||
</SettlementPageHeader>
|
||||
|
||||
<div class="settlement-metric-grid">
|
||||
<SettlementMetricCard label="结算单总数" :value="settlementRows.length" suffix="个" icon="el-icon-tickets" />
|
||||
<SettlementMetricCard label="销售金额合计" :value="money(totals.sales)" variant="emerald" icon="el-icon-wallet" />
|
||||
<SettlementMetricCard label="结算金额合计" :value="money(totals.settlement)" variant="amber" icon="el-icon-s-order" />
|
||||
<SettlementMetricCard label="平台收入合计" :value="money(totals.income)" variant="violet" icon="el-icon-s-marketing" />
|
||||
</div>
|
||||
|
||||
<section class="settlement-card settlement-card--padded" style="margin-bottom: 24px;">
|
||||
<div class="settlement-card-heading" style="margin-bottom: 22px;">
|
||||
<i class="el-icon-s-operation" />
|
||||
<span>结算流程闭环</span>
|
||||
<small class="settlement-card-subtitle">· 点击任一节点进入对应功能</small>
|
||||
</div>
|
||||
<div class="settlement-flow-grid">
|
||||
<div v-for="step in flowSteps" :key="step.index" class="settlement-flow-node" style="cursor: pointer;" @click="goStep(step.route)">
|
||||
<div class="settlement-flow-icon">
|
||||
<i :class="step.icon" />
|
||||
<span class="settlement-flow-index">{{ step.index }}</span>
|
||||
</div>
|
||||
<div class="settlement-card settlement-card--hover settlement-flow-content">
|
||||
<h3>{{ step.title }}</h3>
|
||||
<strong>{{ step.subtitle }}</strong>
|
||||
<p>{{ step.description }}</p>
|
||||
<code class="settlement-flow-endpoint">{{ step.endpoint }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 18px; margin-top: 21px; padding-top: 17px; color: #64748b; font-size: 12px; border-top: 1px solid #e2e8f0;">
|
||||
<span><i class="el-icon-s-data" style="color: #0ea5e9;" /> 数据查询</span>
|
||||
<span><i class="el-icon-document-add" style="color: #f59e0b;" /> 结算单操作</span>
|
||||
<span><i class="el-icon-circle-check" style="color: #10b981;" /> 审批记账</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="settlement-two-column">
|
||||
<section class="settlement-card settlement-card--padded">
|
||||
<div class="settlement-card-heading" style="margin-bottom: 9px;">
|
||||
<i class="el-icon-data-analysis" />
|
||||
<span>状态分布</span>
|
||||
</div>
|
||||
<div v-for="item in statusRows" :key="item.status" class="settlement-status-row">
|
||||
<SettlementStatusBadge :status="item.status" />
|
||||
<div class="settlement-progress-track">
|
||||
<div class="settlement-progress-value" :class="item.color" :style="{ width: item.width }" />
|
||||
</div>
|
||||
<span class="settlement-progress-number">{{ item.value }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settlement-card settlement-card--padded">
|
||||
<div class="settlement-card-heading">
|
||||
<i class="el-icon-video-play" />
|
||||
<span>快速操作</span>
|
||||
</div>
|
||||
<button v-for="item in quickActions" :key="item.title" type="button" class="settlement-quick-action" @click="goStep(item.route)">
|
||||
<b><i :class="item.icon" style="margin-right: 6px; color: #0284c7;" />{{ item.title }}</b>
|
||||
<span>{{ item.description }}</span>
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="settlement-card">
|
||||
<div class="settlement-card-head">
|
||||
<div class="settlement-card-heading">
|
||||
<i class="el-icon-document" />
|
||||
<span>最近结算单</span>
|
||||
<small class="settlement-card-subtitle">· {{ settlementRows.length }} 条</small>
|
||||
</div>
|
||||
<button type="button" class="settlement-btn settlement-btn--ghost" @click="goStep('FinancialSettlementStatementList')">
|
||||
查看全部<i class="el-icon-arrow-right" style="margin: 0 0 0 5px;" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="settlement-table-wrap">
|
||||
<table class="settlement-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>结算单号</th>
|
||||
<th>对手方</th>
|
||||
<th>账期</th>
|
||||
<th>类型</th>
|
||||
<th class="is-number">销售金额</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in recentSettlements" :key="row.id" style="cursor: pointer;" @click="goDetail(row)">
|
||||
<td class="is-mono" style="font-weight: 700;">{{ row.no }}</td>
|
||||
<td>{{ row.name }}</td>
|
||||
<td class="is-mono is-muted">{{ row.start }} ~ {{ row.end }}</td>
|
||||
<td><span class="settlement-pill">{{ row.periodType }}</span></td>
|
||||
<td class="is-number">{{ row.sales }}</td>
|
||||
<td><SettlementStatusBadge :status="row.status" /></td>
|
||||
</tr>
|
||||
<tr v-if="!loading && !recentSettlements.length">
|
||||
<td colspan="6" style="padding: 42px; color: #94a3b8; text-align: center;">暂无结算单数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</SettlementPageFrame>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettlementPageFrame from './SettlementPageFrame'
|
||||
import SettlementPageHeader from './SettlementPageHeader'
|
||||
import SettlementMetricCard from './SettlementMetricCard'
|
||||
import SettlementStatusBadge from './SettlementStatusBadge'
|
||||
import { settlementListAPI } from '@/api/FinancialSettlementCenter'
|
||||
import {
|
||||
getAccountingOrgId,
|
||||
getItems,
|
||||
getPayload,
|
||||
money,
|
||||
normalizeSettlement
|
||||
} from './settlementHelpers'
|
||||
import { flowSteps } from './settlementMockData'
|
||||
|
||||
export default {
|
||||
name: 'ProcessOverview',
|
||||
components: {
|
||||
SettlementPageFrame,
|
||||
SettlementPageHeader,
|
||||
SettlementMetricCard,
|
||||
SettlementStatusBadge
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
flowSteps: flowSteps.map((step, index) => ({
|
||||
...step,
|
||||
route: [
|
||||
'FinancialSettlementSummaryQuery',
|
||||
'FinancialSettlementStatementPreview',
|
||||
'FinancialCreateSettlementStatement',
|
||||
'FinancialSubmitforApproval',
|
||||
'FinancialApprovalCallback'
|
||||
][index]
|
||||
})),
|
||||
settlementRows: [],
|
||||
quickActions: [
|
||||
{ icon: 'el-icon-view', title: '1. 预览结算单', description: '查看账期明细,检查重复', route: 'FinancialSettlementStatementPreview' },
|
||||
{ icon: 'el-icon-circle-plus-outline', title: '2. 创建结算单', description: '生成草稿状态结算单', route: 'FinancialCreateSettlementStatement' },
|
||||
{ icon: 'el-icon-s-promotion', title: '3. 提交审批', description: '送审并等待审批结果', route: 'FinancialSubmitforApproval' },
|
||||
{ icon: 'el-icon-circle-check', title: '4. 审批回调', description: '处理审批结果并完成记账', route: 'FinancialApprovalCallback' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totals() {
|
||||
return this.settlementRows.reduce((result, row) => ({
|
||||
sales: result.sales + row.salesAmount,
|
||||
settlement: result.settlement + row.settlementAmount,
|
||||
income: result.income + row.incomeAmount
|
||||
}), { sales: 0, settlement: 0, income: 0 })
|
||||
},
|
||||
recentSettlements() {
|
||||
return this.settlementRows.slice(0, 5)
|
||||
},
|
||||
statusRows() {
|
||||
const statusList = ['draft', 'approving', 'approved', 'settled', 'rejected', 'failed', 'cancelled']
|
||||
const colorMap = {
|
||||
approving: 'settlement-progress-value--amber',
|
||||
approved: 'settlement-progress-value--emerald',
|
||||
rejected: 'settlement-progress-value--rose',
|
||||
failed: 'settlement-progress-value--rose'
|
||||
}
|
||||
const total = this.settlementRows.length
|
||||
return statusList.map(status => {
|
||||
const count = this.settlementRows.filter(row => row.status === status).length
|
||||
const ratio = total ? (count / total) * 100 : 0
|
||||
return {
|
||||
status,
|
||||
width: `${ratio}%`,
|
||||
value: `${count} (${ratio.toFixed(0)}%)`,
|
||||
color: colorMap[status] || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
money,
|
||||
async loadData() {
|
||||
const accountingOrgid = getAccountingOrgId(this.$route)
|
||||
if (!accountingOrgid) {
|
||||
this.$message.warning('未获取到核算机构 ID')
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const payload = getPayload(await settlementListAPI({
|
||||
accounting_orgid: accountingOrgid,
|
||||
current_page: 1,
|
||||
page_size: 100
|
||||
}))
|
||||
this.settlementRows = getItems(payload).map(normalizeSettlement)
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '结算单概览加载失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
goStep(route) {
|
||||
if (route) this.$router.push({ name: route })
|
||||
},
|
||||
goDetail(row) {
|
||||
this.$router.push({
|
||||
name: 'FinancialSettlementStatementDetails',
|
||||
query: { id: row.id }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="settlement-metric-card" :class="`settlement-metric-card--${variant}`">
|
||||
<span v-if="icon" class="settlement-metric-icon">
|
||||
<i :class="icon" />
|
||||
</span>
|
||||
<div class="settlement-metric-label">{{ label }}</div>
|
||||
<div class="settlement-metric-value">
|
||||
{{ value }}<span v-if="suffix" class="settlement-metric-suffix">{{ suffix }}</span>
|
||||
</div>
|
||||
<div v-if="hint" class="settlement-metric-hint">{{ hint }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SettlementMetricCard',
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
value: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hint: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator: value => ['primary', 'emerald', 'amber', 'violet'].includes(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="settlement-page-header">
|
||||
<div class="settlement-page-heading">
|
||||
<span class="settlement-page-icon">
|
||||
<i :class="icon" />
|
||||
</span>
|
||||
<div class="settlement-page-title">
|
||||
<h1>{{ title }}</h1>
|
||||
<p>{{ description }}</p>
|
||||
<span v-if="endpoint" class="settlement-endpoint">{{ endpoint }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$slots.actions" class="settlement-header-actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SettlementPageHeader',
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'el-icon-s-data'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
endpoint: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<SettlementPageFrame>
|
||||
<SettlementPageHeader
|
||||
icon="el-icon-document"
|
||||
title="结算单详情"
|
||||
description="查询结算单主表信息和明细快照数据。"
|
||||
endpoint="GET /bill/finance_settlement_detail.dspy"
|
||||
>
|
||||
<template #actions>
|
||||
<button type="button" class="settlement-btn settlement-btn--primary" :disabled="loadingDetail" @click="loadDetail">
|
||||
<i class="el-icon-refresh" />{{ loadingDetail ? '刷新中...' : '刷新' }}
|
||||
</button>
|
||||
</template>
|
||||
</SettlementPageHeader>
|
||||
|
||||
<div class="settlement-split-layout">
|
||||
<aside class="settlement-card settlement-selector">
|
||||
<div class="settlement-card-head">
|
||||
<div class="settlement-card-heading">
|
||||
<i class="el-icon-tickets" />
|
||||
<span>结算单</span>
|
||||
<small class="settlement-card-subtitle">· {{ settlementRows.length }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-for="row in settlementRows"
|
||||
:key="row.id"
|
||||
type="button"
|
||||
class="settlement-selector-item"
|
||||
:class="{ 'is-active': selectedRow && row.id === selectedRow.id }"
|
||||
@click="selectRow(row)"
|
||||
>
|
||||
<span class="settlement-selector-no">{{ row.no }}</span>
|
||||
<span class="settlement-selector-name">{{ row.name }}</span>
|
||||
<span class="settlement-selector-date">{{ row.start }} ~ {{ row.end }}</span>
|
||||
<span style="display: block; margin-top: 7px;"><SettlementStatusBadge :status="row.status" /></span>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<main style="min-width: 0;">
|
||||
<template v-if="selectedRow">
|
||||
<section class="settlement-card" style="margin-bottom: 20px;">
|
||||
<div class="settlement-detail-head">
|
||||
<div>
|
||||
<div class="settlement-detail-no">
|
||||
<i class="el-icon-document" style="color: #94a3b8; font-size: 17px;" />
|
||||
{{ selectedRow.no }}
|
||||
<SettlementStatusBadge :status="selectedRow.status" />
|
||||
</div>
|
||||
<div class="settlement-detail-meta">创建时间:<span class="is-mono">{{ selectedRow.createdAt }}</span></div>
|
||||
</div>
|
||||
<button v-if="isSubmittable(selectedRow.status)" type="button" class="settlement-btn settlement-btn--primary" @click="goSubmit">
|
||||
<i class="el-icon-s-promotion" />提交审批
|
||||
</button>
|
||||
</div>
|
||||
<div class="settlement-info-grid">
|
||||
<div>
|
||||
<div class="settlement-info-label">对手方类型</div>
|
||||
<div class="settlement-info-value"><i class="el-icon-office-building" style="margin-right: 5px; color: #94a3b8;" />{{ selectedRow.type }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="settlement-info-label">对手方 ID</div>
|
||||
<div class="settlement-info-value is-mono">{{ selectedRow.orgId }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="settlement-info-label">对手方名称</div>
|
||||
<div class="settlement-info-value">{{ selectedRow.name }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="settlement-info-label">账期类型</div>
|
||||
<div class="settlement-info-value"><i class="el-icon-date" style="margin-right: 5px; color: #94a3b8;" />{{ selectedRow.periodType }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="settlement-info-label">账期开始</div>
|
||||
<div class="settlement-info-value is-mono">{{ selectedRow.start }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="settlement-info-label">账期结束</div>
|
||||
<div class="settlement-info-value is-mono">{{ selectedRow.end }}</div>
|
||||
</div>
|
||||
<div v-if="selectedRow.approvalId">
|
||||
<div class="settlement-info-label">审批 ID</div>
|
||||
<div class="settlement-info-value is-mono">{{ selectedRow.approvalId }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="settlement-metric-grid settlement-metric-grid--three">
|
||||
<SettlementMetricCard label="销售金额" :value="selectedRow.sales" icon="el-icon-wallet" />
|
||||
<SettlementMetricCard label="结算金额" :value="selectedRow.settlement" variant="emerald" icon="el-icon-s-order" />
|
||||
<SettlementMetricCard label="平台收入" :value="selectedRow.income" variant="amber" icon="el-icon-s-marketing" />
|
||||
</div>
|
||||
|
||||
<section class="settlement-card">
|
||||
<div class="settlement-card-head">
|
||||
<div class="settlement-card-heading">
|
||||
<i class="el-icon-document" />
|
||||
<span>结算明细快照</span>
|
||||
<small class="settlement-card-subtitle">· {{ detailTotal }} 笔</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settlement-table-wrap">
|
||||
<table class="settlement-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>账单 ID</th>
|
||||
<th>订单 ID</th>
|
||||
<th>账单日期</th>
|
||||
<th>销售模式</th>
|
||||
<th class="is-number">销售金额</th>
|
||||
<th class="is-number">结算金额</th>
|
||||
<th class="is-number">平台收入</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in billRows" :key="row.billId">
|
||||
<td class="is-mono">{{ row.billId }}</td>
|
||||
<td class="is-mono">{{ row.orderId }}</td>
|
||||
<td class="is-mono">{{ row.date }}</td>
|
||||
<td><span class="settlement-pill">{{ row.mode }}</span></td>
|
||||
<td class="is-number">{{ row.sales }}</td>
|
||||
<td class="is-number is-emerald">{{ row.settlement }}</td>
|
||||
<td class="is-number is-amber">{{ row.income }}</td>
|
||||
</tr>
|
||||
<tr v-if="!loadingDetail && !billRows.length">
|
||||
<td colspan="7" style="padding: 42px; color: #94a3b8; text-align: center;">暂无结算明细</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="settlement-table-footer">
|
||||
<span>第 {{ currentPage }} 页 · 每页 {{ pageSize }} 条 · 共 {{ detailTotal }} 条</span>
|
||||
<div class="settlement-pagination">
|
||||
<button type="button" class="settlement-btn" :disabled="currentPage <= 1 || loadingDetail" @click="changePage(-1)"><i class="el-icon-arrow-left" style="margin: 0;" /></button>
|
||||
<span class="settlement-page-number">{{ currentPage }} / {{ totalPages }}</span>
|
||||
<button type="button" class="settlement-btn" :disabled="currentPage >= totalPages || loadingDetail" @click="changePage(1)"><i class="el-icon-arrow-right" style="margin: 0;" /></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<section v-else class="settlement-card settlement-card--padded" style="color: #94a3b8; text-align: center;">
|
||||
{{ loadingList ? '正在加载结算单...' : '暂无可查看的结算单' }}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</SettlementPageFrame>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettlementPageFrame from './SettlementPageFrame'
|
||||
import SettlementPageHeader from './SettlementPageHeader'
|
||||
import SettlementMetricCard from './SettlementMetricCard'
|
||||
import SettlementStatusBadge from './SettlementStatusBadge'
|
||||
import { settlementDetailsAPI, settlementListAPI } from '@/api/FinancialSettlementCenter'
|
||||
import {
|
||||
getAccountingOrgId,
|
||||
getCurrentPage,
|
||||
getItems,
|
||||
getPageSize,
|
||||
getPayload,
|
||||
getTotal,
|
||||
isSubmittable,
|
||||
normalizeBill,
|
||||
normalizeSettlement
|
||||
} from './settlementHelpers'
|
||||
|
||||
export default {
|
||||
name: 'SettlementStatementDetails',
|
||||
components: {
|
||||
SettlementPageFrame,
|
||||
SettlementPageHeader,
|
||||
SettlementMetricCard,
|
||||
SettlementStatusBadge
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loadingList: false,
|
||||
loadingDetail: false,
|
||||
settlementRows: [],
|
||||
selectedRow: null,
|
||||
billRows: [],
|
||||
detailTotal: 0,
|
||||
currentPage: 1,
|
||||
pageSize: 100
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalPages() {
|
||||
return Math.max(1, Math.ceil(this.detailTotal / this.pageSize))
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadList()
|
||||
},
|
||||
methods: {
|
||||
isSubmittable,
|
||||
async loadList() {
|
||||
const accountingOrgid = getAccountingOrgId(this.$route)
|
||||
if (!accountingOrgid) {
|
||||
this.$message.warning('未获取到核算机构 ID')
|
||||
return
|
||||
}
|
||||
|
||||
this.loadingList = true
|
||||
try {
|
||||
const payload = getPayload(await settlementListAPI({
|
||||
accounting_orgid: accountingOrgid,
|
||||
current_page: 1,
|
||||
page_size: 100
|
||||
}))
|
||||
this.settlementRows = getItems(payload).map(normalizeSettlement)
|
||||
const routeId = this.$route.query.id
|
||||
this.selectedRow = this.settlementRows.find(item => item.id === routeId) || this.settlementRows[0] || null
|
||||
if (this.selectedRow) await this.loadDetail()
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '结算单列表查询失败')
|
||||
} finally {
|
||||
this.loadingList = false
|
||||
}
|
||||
},
|
||||
async loadDetail() {
|
||||
if (!this.selectedRow || !this.selectedRow.id) return
|
||||
this.loadingDetail = true
|
||||
try {
|
||||
const payload = getPayload(await settlementDetailsAPI({
|
||||
settlement_id: this.selectedRow.id,
|
||||
current_page: this.currentPage,
|
||||
page_size: this.pageSize
|
||||
}))
|
||||
const settlement = payload.settlement || payload.header || payload
|
||||
if (settlement && typeof settlement === 'object' && !Array.isArray(settlement)) {
|
||||
this.selectedRow = normalizeSettlement(settlement)
|
||||
}
|
||||
this.billRows = getItems(payload).map(normalizeBill)
|
||||
this.detailTotal = getTotal(payload, this.billRows.length)
|
||||
this.currentPage = getCurrentPage(payload, this.currentPage)
|
||||
this.pageSize = getPageSize(payload, this.pageSize)
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '结算单详情查询失败')
|
||||
} finally {
|
||||
this.loadingDetail = false
|
||||
}
|
||||
},
|
||||
selectRow(row) {
|
||||
if (!row || row.id === (this.selectedRow && this.selectedRow.id)) return
|
||||
this.selectedRow = row
|
||||
this.currentPage = 1
|
||||
this.billRows = []
|
||||
this.$router.replace({
|
||||
name: 'FinancialSettlementStatementDetails',
|
||||
query: { id: row.id }
|
||||
})
|
||||
this.loadDetail()
|
||||
},
|
||||
changePage(offset) {
|
||||
const nextPage = this.currentPage + offset
|
||||
if (nextPage < 1 || nextPage > this.totalPages) return
|
||||
this.currentPage = nextPage
|
||||
this.loadDetail()
|
||||
},
|
||||
goSubmit() {
|
||||
this.$router.push({
|
||||
name: 'FinancialSubmitforApproval',
|
||||
query: { id: this.selectedRow.id }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<SettlementPageFrame>
|
||||
<SettlementPageHeader
|
||||
icon="el-icon-tickets"
|
||||
title="结算单列表"
|
||||
description="分页查询结算单主表数据,支持按对手方、状态、账期等条件筛选。"
|
||||
endpoint="GET /bill/finance_settlement_list.dspy"
|
||||
>
|
||||
<template #actions>
|
||||
<button type="button" class="settlement-btn" :disabled="loading" @click="resetQuery">
|
||||
<i class="el-icon-refresh" />重置
|
||||
</button>
|
||||
<button type="button" class="settlement-btn settlement-btn--primary" :disabled="loading" @click="search">
|
||||
<i class="el-icon-search" />{{ loading ? '查询中...' : '查询' }}
|
||||
</button>
|
||||
</template>
|
||||
</SettlementPageHeader>
|
||||
|
||||
<section class="settlement-card settlement-card--padded" style="margin-bottom: 24px;">
|
||||
<div class="settlement-card-heading" style="margin-bottom: 18px;">
|
||||
<i class="el-icon-filter" />
|
||||
<span>筛选条件</span>
|
||||
</div>
|
||||
<div class="settlement-filter-grid settlement-filter-grid--six">
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">对手方类型</label>
|
||||
<select v-model="form.counterparty_type" class="settlement-select">
|
||||
<option value="">全部</option>
|
||||
<option value="supplier">供应商</option>
|
||||
<option value="reseller">分销商</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">对手方</label>
|
||||
<input v-model.trim="form.counterparty_orgid" class="settlement-input" type="text" placeholder="可选:对手方机构 ID">
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">状态</label>
|
||||
<select v-model="form.status" class="settlement-select">
|
||||
<option value="">全部</option>
|
||||
<option v-for="item in statusOptions" :key="item.value" :value="item.value">{{ item.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">账期类型</label>
|
||||
<select v-model="form.period_type" class="settlement-select">
|
||||
<option value="">全部</option>
|
||||
<option value="day">日结</option>
|
||||
<option value="month">月结</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">开始日期</label>
|
||||
<input v-model="form.start_date" class="settlement-input" type="date">
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">结束日期</label>
|
||||
<input v-model="form.end_date" class="settlement-input" type="date">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="settlement-metric-grid settlement-metric-grid--three">
|
||||
<SettlementMetricCard label="销售金额合计" :value="money(pageTotals.sales)" :hint="`当前页 ${settlementRows.length} 条`" icon="el-icon-wallet" />
|
||||
<SettlementMetricCard label="结算金额合计" :value="money(pageTotals.settlement)" variant="emerald" icon="el-icon-s-order" />
|
||||
<SettlementMetricCard label="平台收入合计" :value="money(pageTotals.income)" variant="amber" icon="el-icon-s-marketing" />
|
||||
</div>
|
||||
|
||||
<section class="settlement-card">
|
||||
<div class="settlement-card-head">
|
||||
<div class="settlement-card-heading">
|
||||
<i class="el-icon-tickets" />
|
||||
<span>结算单列表</span>
|
||||
<small class="settlement-card-subtitle">· 共 {{ total }} 条</small>
|
||||
</div>
|
||||
<button type="button" class="settlement-btn settlement-btn--ghost" disabled>
|
||||
<i class="el-icon-download" />导出
|
||||
</button>
|
||||
</div>
|
||||
<div class="settlement-table-wrap">
|
||||
<table class="settlement-table" style="min-width: 1180px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>结算单号</th>
|
||||
<th>对手方</th>
|
||||
<th>账期</th>
|
||||
<th>账期类型</th>
|
||||
<th class="is-number">销售金额</th>
|
||||
<th class="is-number">结算金额</th>
|
||||
<th class="is-number">平台收入</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th class="is-number">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in settlementRows" :key="row.id">
|
||||
<td class="is-mono" style="font-weight: 700;">{{ row.no }}</td>
|
||||
<td>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span
|
||||
style="display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 7px;"
|
||||
:style="{ color: row.typeKey === 'supplier' ? '#0284c7' : '#7c3aed', background: row.typeKey === 'supplier' ? '#f0f9ff' : '#f5f3ff' }"
|
||||
><i class="el-icon-office-building" /></span>
|
||||
<span>
|
||||
<b style="display: block; color: #334155; font-size: 12px;">{{ row.name }}</b>
|
||||
<small class="is-mono is-muted">{{ row.orgId }}</small>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="is-mono is-muted">{{ row.start }} ~ {{ row.end }}</td>
|
||||
<td><span class="settlement-pill">{{ row.periodType }}</span></td>
|
||||
<td class="is-number">{{ row.sales }}</td>
|
||||
<td class="is-number is-emerald">{{ row.settlement }}</td>
|
||||
<td class="is-number is-amber">{{ row.income }}</td>
|
||||
<td>
|
||||
<SettlementStatusBadge :status="row.status" />
|
||||
<small v-if="row.reason" style="display: block; max-width: 146px; margin-top: 5px; overflow: hidden; color: #e11d48; font-size: 10px; text-overflow: ellipsis; white-space: nowrap;">{{ row.reason }}</small>
|
||||
</td>
|
||||
<td class="is-mono is-muted">{{ row.createdAt }}</td>
|
||||
<td class="is-number">
|
||||
<button type="button" class="settlement-btn settlement-btn--ghost" title="查看详情" @click="openDetail(row)"><i class="el-icon-view" style="margin: 0;" /></button>
|
||||
<button v-if="isSubmittable(row.status)" type="button" class="settlement-btn settlement-btn--ghost" title="提交审批" @click="goSubmit(row)"><i class="el-icon-s-promotion" style="margin: 0;" /></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!loading && !settlementRows.length">
|
||||
<td colspan="10" style="padding: 42px; color: #94a3b8; text-align: center;">暂无结算单数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="settlement-table-footer">
|
||||
<span>第 {{ form.current_page }} 页 · 每页 {{ form.page_size }} 条 · 共 {{ total }} 条</span>
|
||||
<div class="settlement-pagination">
|
||||
<button type="button" class="settlement-btn" :disabled="form.current_page <= 1 || loading" @click="changePage(-1)"><i class="el-icon-arrow-left" style="margin: 0;" /></button>
|
||||
<span class="settlement-page-number">{{ form.current_page }} / {{ totalPages }}</span>
|
||||
<button type="button" class="settlement-btn" :disabled="form.current_page >= totalPages || loading" @click="changePage(1)"><i class="el-icon-arrow-right" style="margin: 0;" /></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</SettlementPageFrame>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettlementPageFrame from './SettlementPageFrame'
|
||||
import SettlementPageHeader from './SettlementPageHeader'
|
||||
import SettlementMetricCard from './SettlementMetricCard'
|
||||
import SettlementStatusBadge from './SettlementStatusBadge'
|
||||
import { settlementListAPI } from '@/api/FinancialSettlementCenter'
|
||||
import {
|
||||
getAccountingOrgId,
|
||||
getCurrentPage,
|
||||
getItems,
|
||||
getPageSize,
|
||||
getPayload,
|
||||
getTotal,
|
||||
isSubmittable,
|
||||
money,
|
||||
normalizeSettlement
|
||||
} from './settlementHelpers'
|
||||
|
||||
export default {
|
||||
name: 'SettlementStatementList',
|
||||
components: {
|
||||
SettlementPageFrame,
|
||||
SettlementPageHeader,
|
||||
SettlementMetricCard,
|
||||
SettlementStatusBadge
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
total: 0,
|
||||
settlementRows: [],
|
||||
statusOptions: [
|
||||
{ value: 'draft', label: '草稿' },
|
||||
{ value: 'approving', label: '审批中' },
|
||||
{ value: 'approved', label: '已审批' },
|
||||
{ value: 'rejected', label: '已拒绝' },
|
||||
{ value: 'settled', label: '已结算' },
|
||||
{ value: 'failed', label: '结算失败' },
|
||||
{ value: 'cancelled', label: '已撤销' }
|
||||
],
|
||||
form: {
|
||||
counterparty_type: '',
|
||||
counterparty_orgid: '',
|
||||
status: '',
|
||||
period_type: '',
|
||||
start_date: '2026-06-01',
|
||||
end_date: '2026-06-30',
|
||||
current_page: 1,
|
||||
page_size: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalPages() {
|
||||
return Math.max(1, Math.ceil(this.total / this.form.page_size))
|
||||
},
|
||||
pageTotals() {
|
||||
return this.settlementRows.reduce((totals, row) => ({
|
||||
sales: totals.sales + row.salesAmount,
|
||||
settlement: totals.settlement + row.settlementAmount,
|
||||
income: totals.income + row.incomeAmount
|
||||
}), { sales: 0, settlement: 0, income: 0 })
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
money,
|
||||
isSubmittable,
|
||||
getParams() {
|
||||
return {
|
||||
accounting_orgid: getAccountingOrgId(this.$route),
|
||||
counterparty_type: this.form.counterparty_type || undefined,
|
||||
counterparty_orgid: this.form.counterparty_orgid || undefined,
|
||||
status: this.form.status || undefined,
|
||||
period_type: this.form.period_type || undefined,
|
||||
start_date: this.form.start_date || undefined,
|
||||
end_date: this.form.end_date || undefined,
|
||||
current_page: this.form.current_page,
|
||||
page_size: this.form.page_size
|
||||
}
|
||||
},
|
||||
async loadData() {
|
||||
const params = this.getParams()
|
||||
if (!params.accounting_orgid) {
|
||||
this.$message.warning('未获取到核算机构 ID')
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const payload = getPayload(await settlementListAPI(params))
|
||||
this.settlementRows = getItems(payload).map(normalizeSettlement)
|
||||
this.total = getTotal(payload, this.settlementRows.length)
|
||||
this.form.current_page = getCurrentPage(payload, this.form.current_page)
|
||||
this.form.page_size = getPageSize(payload, this.form.page_size)
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '结算单列表查询失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
search() {
|
||||
this.form.current_page = 1
|
||||
this.loadData()
|
||||
},
|
||||
resetQuery() {
|
||||
this.form = {
|
||||
counterparty_type: '',
|
||||
counterparty_orgid: '',
|
||||
status: '',
|
||||
period_type: '',
|
||||
start_date: '2026-06-01',
|
||||
end_date: '2026-06-30',
|
||||
current_page: 1,
|
||||
page_size: 20
|
||||
}
|
||||
this.loadData()
|
||||
},
|
||||
changePage(offset) {
|
||||
const nextPage = this.form.current_page + offset
|
||||
if (nextPage < 1 || nextPage > this.totalPages) return
|
||||
this.form.current_page = nextPage
|
||||
this.loadData()
|
||||
},
|
||||
openDetail(row) {
|
||||
this.$router.push({
|
||||
name: 'FinancialSettlementStatementDetails',
|
||||
query: { id: row.id }
|
||||
})
|
||||
},
|
||||
goSubmit(row) {
|
||||
this.$router.push({
|
||||
name: 'FinancialSubmitforApproval',
|
||||
query: { id: row.id }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<SettlementPageFrame>
|
||||
<SettlementPageHeader
|
||||
icon="el-icon-view"
|
||||
title="结算单预览"
|
||||
description="创建结算单前预览明细,检查指定账期是否已有结算单,并返回可结算账单明细。"
|
||||
endpoint="GET /bill/finance_settlement_preview.dspy"
|
||||
>
|
||||
<template #actions>
|
||||
<button type="button" class="settlement-btn settlement-btn--primary" :disabled="loading" @click="loadPreview">
|
||||
<i class="el-icon-view" />{{ loading ? '预览中...' : '预览' }}
|
||||
</button>
|
||||
</template>
|
||||
</SettlementPageHeader>
|
||||
|
||||
<section class="settlement-card settlement-card--padded" style="margin-bottom: 24px;">
|
||||
<div class="settlement-card-heading" style="margin-bottom: 18px;">
|
||||
<i class="el-icon-filter" />
|
||||
<span>预览条件</span>
|
||||
</div>
|
||||
<div class="settlement-filter-grid">
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">对手方类型</label>
|
||||
<div class="settlement-choice-group">
|
||||
<button type="button" class="settlement-choice" :class="{ 'is-active': form.counterparty_type === 'supplier' }" @click="form.counterparty_type = 'supplier'">供应商</button>
|
||||
<button type="button" class="settlement-choice" :class="{ 'is-active': form.counterparty_type === 'reseller' }" @click="form.counterparty_type = 'reseller'">分销商</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">对手方</label>
|
||||
<input v-model.trim="form.counterparty_orgid" class="settlement-input" type="text" placeholder="请输入供应商或分销商机构 ID">
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">账期类型</label>
|
||||
<div class="settlement-choice-group">
|
||||
<button type="button" class="settlement-choice" :class="{ 'is-active': form.period_type === 'day' }" @click="form.period_type = 'day'">日结</button>
|
||||
<button type="button" class="settlement-choice" :class="{ 'is-active': form.period_type === 'month' }" @click="form.period_type = 'month'">月结</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">账期开始</label>
|
||||
<input v-model="form.period_start" class="settlement-input" type="date">
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">账期结束</label>
|
||||
<input v-model="form.period_end" class="settlement-input" type="date">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="previewed && existingSettlement" class="settlement-alert settlement-alert--warning">
|
||||
<span class="settlement-alert-icon"><i class="el-icon-warning-outline" /></span>
|
||||
<div class="settlement-alert-content">
|
||||
<b>该账期已存在结算单</b>
|
||||
<p>结算单号:{{ existingSettlement.no }},当前状态:<SettlementStatusBadge :status="existingSettlement.status" /></p>
|
||||
</div>
|
||||
</section>
|
||||
<section v-else-if="previewed" class="settlement-alert">
|
||||
<span class="settlement-alert-icon"><i class="el-icon-circle-check" /></span>
|
||||
<div class="settlement-alert-content">
|
||||
<b>可创建结算单</b>
|
||||
<p>当前账期未存在结算单,可以创建新的结算单。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="settlement-metric-grid">
|
||||
<SettlementMetricCard label="销售金额" :value="summary.sales" icon="el-icon-wallet" />
|
||||
<SettlementMetricCard label="应结算金额" :value="summary.settlement" variant="emerald" icon="el-icon-s-order" />
|
||||
<SettlementMetricCard label="平台收入" :value="summary.income" variant="amber" icon="el-icon-s-marketing" />
|
||||
<SettlementMetricCard label="账单数量" :value="summary.billCount" suffix="笔" variant="violet" icon="el-icon-document" />
|
||||
</div>
|
||||
|
||||
<section class="settlement-card">
|
||||
<div class="settlement-card-head">
|
||||
<div class="settlement-card-heading">
|
||||
<i class="el-icon-document" />
|
||||
<span>可结算账单明细</span>
|
||||
<small class="settlement-card-subtitle">· {{ billRows.length }} 笔</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settlement-table-wrap">
|
||||
<table class="settlement-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>账单 ID</th>
|
||||
<th>订单 ID</th>
|
||||
<th>账单日期</th>
|
||||
<th>销售模式</th>
|
||||
<th class="is-number">销售金额</th>
|
||||
<th class="is-number">结算金额</th>
|
||||
<th class="is-number">平台收入</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in billRows" :key="row.billId">
|
||||
<td class="is-mono">{{ row.billId }}</td>
|
||||
<td class="is-mono">{{ row.orderId }}</td>
|
||||
<td><span class="is-mono"><i class="el-icon-date" style="margin-right: 4px; color: #94a3b8;" />{{ row.date }}</span></td>
|
||||
<td><span class="settlement-pill">{{ row.mode }}</span></td>
|
||||
<td class="is-number">{{ row.sales }}</td>
|
||||
<td class="is-number is-emerald">{{ row.settlement }}</td>
|
||||
<td class="is-number is-amber">{{ row.income }}</td>
|
||||
</tr>
|
||||
<tr v-if="previewed && !loading && !billRows.length">
|
||||
<td colspan="7" style="padding: 42px; color: #94a3b8; text-align: center;">暂无可结算账单</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</SettlementPageFrame>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettlementPageFrame from './SettlementPageFrame'
|
||||
import SettlementPageHeader from './SettlementPageHeader'
|
||||
import SettlementMetricCard from './SettlementMetricCard'
|
||||
import SettlementStatusBadge from './SettlementStatusBadge'
|
||||
import { settlementPreviewAPI } from '@/api/FinancialSettlementCenter'
|
||||
import {
|
||||
getAccountingOrgId,
|
||||
getItems,
|
||||
getPayload,
|
||||
getSummary,
|
||||
normalizeBill,
|
||||
normalizeSettlement
|
||||
} from './settlementHelpers'
|
||||
|
||||
export default {
|
||||
name: 'SettlementStatementPreview',
|
||||
components: {
|
||||
SettlementPageFrame,
|
||||
SettlementPageHeader,
|
||||
SettlementMetricCard,
|
||||
SettlementStatusBadge
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
previewed: false,
|
||||
existingSettlement: null,
|
||||
billRows: [],
|
||||
summary: {
|
||||
sales: '¥0.00',
|
||||
settlement: '¥0.00',
|
||||
income: '¥0.00',
|
||||
billCount: 0
|
||||
},
|
||||
form: {
|
||||
counterparty_type: 'reseller',
|
||||
counterparty_orgid: '',
|
||||
period_type: 'month',
|
||||
period_start: '2026-06-01',
|
||||
period_end: '2026-06-30',
|
||||
current_page: 1,
|
||||
page_size: 50
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadPreview() {
|
||||
const accountingOrgid = getAccountingOrgId(this.$route)
|
||||
if (!accountingOrgid) {
|
||||
this.$message.warning('未获取到核算机构 ID')
|
||||
return
|
||||
}
|
||||
if (!this.form.counterparty_orgid) {
|
||||
this.$message.warning('请输入对手方机构 ID')
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const payload = getPayload(await settlementPreviewAPI({
|
||||
accounting_orgid: accountingOrgid,
|
||||
counterparty_type: this.form.counterparty_type,
|
||||
counterparty_orgid: this.form.counterparty_orgid,
|
||||
period_type: this.form.period_type,
|
||||
period_start: this.form.period_start,
|
||||
period_end: this.form.period_end,
|
||||
current_page: this.form.current_page,
|
||||
page_size: this.form.page_size
|
||||
}))
|
||||
this.summary = getSummary(payload)
|
||||
this.billRows = getItems(payload).map(normalizeBill)
|
||||
const existing = payload.existing_settlement || payload.existingSettlement
|
||||
this.existingSettlement = existing ? normalizeSettlement(existing) : null
|
||||
this.previewed = true
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '结算单预览失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<span class="settlement-status" :class="`settlement-status--${status}`">
|
||||
<i :class="statusIcon" />
|
||||
{{ statusLabel }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const STATUS_MAP = {
|
||||
draft: { label: '草稿', icon: 'el-icon-document' },
|
||||
approving: { label: '审批中', icon: 'el-icon-time' },
|
||||
approved: { label: '已审批', icon: 'el-icon-circle-check' },
|
||||
rejected: { label: '已拒绝', icon: 'el-icon-circle-close' },
|
||||
settled: { label: '已结算', icon: 'el-icon-success' },
|
||||
failed: { label: '结算失败', icon: 'el-icon-warning-outline' },
|
||||
cancelled: { label: '已撤销', icon: 'el-icon-remove-outline' }
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'SettlementStatusBadge',
|
||||
props: {
|
||||
status: {
|
||||
type: String,
|
||||
default: 'draft'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
statusConfig() {
|
||||
return STATUS_MAP[this.status] || STATUS_MAP.draft
|
||||
},
|
||||
statusLabel() {
|
||||
return this.statusConfig.label
|
||||
},
|
||||
statusIcon() {
|
||||
return this.statusConfig.icon
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<SettlementPageFrame>
|
||||
<SettlementPageHeader
|
||||
icon="el-icon-s-promotion"
|
||||
title="提交审批"
|
||||
description="将草稿、审批拒绝或结算失败的结算单提交审批。提交成功后状态更新为审批中(approving)。"
|
||||
endpoint="GET /bill/finance_settlement_submit.dspy"
|
||||
>
|
||||
<template #actions>
|
||||
<button type="button" class="settlement-btn settlement-btn--primary" :disabled="loadingList" @click="loadList">
|
||||
<i class="el-icon-refresh" />{{ loadingList ? '刷新中...' : '刷新' }}
|
||||
</button>
|
||||
</template>
|
||||
</SettlementPageHeader>
|
||||
|
||||
<div class="settlement-split-layout">
|
||||
<aside class="settlement-card settlement-selector">
|
||||
<div class="settlement-card-head">
|
||||
<div class="settlement-card-heading">
|
||||
<i class="el-icon-filter" />
|
||||
<span>待提交结算单</span>
|
||||
<small class="settlement-card-subtitle">· 3</small>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-for="row in submitRows"
|
||||
:key="row.id"
|
||||
type="button"
|
||||
class="settlement-selector-item"
|
||||
:class="{ 'is-active': selectedRow && row.id === selectedRow.id }"
|
||||
@click="selectRow(row)"
|
||||
>
|
||||
<span class="settlement-selector-no">{{ row.no }}</span>
|
||||
<span class="settlement-selector-name">{{ row.name }}</span>
|
||||
<span class="settlement-selector-date">{{ row.start }} ~ {{ row.end }}</span>
|
||||
<span style="display: block; margin-top: 7px;"><SettlementStatusBadge :status="row.status" /></span>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<main style="min-width: 0;">
|
||||
<template v-if="selectedRow">
|
||||
<section class="settlement-card" style="margin-bottom: 20px;">
|
||||
<div class="settlement-detail-head">
|
||||
<div>
|
||||
<div class="settlement-detail-no">
|
||||
<i class="el-icon-document" style="color: #0ea5e9; font-size: 17px;" />
|
||||
{{ selectedRow.no }}
|
||||
<SettlementStatusBadge :status="selectedRow.status" />
|
||||
</div>
|
||||
<div class="settlement-detail-meta">
|
||||
<i class="el-icon-office-building" /> {{ selectedRow.name }}
|
||||
<span style="margin: 0 7px; color: #cbd5e1;">|</span>
|
||||
<i class="el-icon-date" /> {{ selectedRow.start }} ~ {{ selectedRow.end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settlement-metric-grid settlement-metric-grid--three" style="margin: 0; padding: 0 20px 20px;">
|
||||
<SettlementMetricCard label="销售金额" :value="selectedRow.sales" icon="el-icon-wallet" />
|
||||
<SettlementMetricCard label="结算金额" :value="selectedRow.settlement" variant="emerald" icon="el-icon-s-order" />
|
||||
<SettlementMetricCard label="平台收入" :value="selectedRow.income" variant="amber" icon="el-icon-s-marketing" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settlement-card">
|
||||
<div class="settlement-form-section">
|
||||
<div class="settlement-section-title">
|
||||
<span class="settlement-section-title-icon"><i class="el-icon-s-promotion" /></span>
|
||||
<div>
|
||||
<b>提交审批</b>
|
||||
<span>填写审批信息后提交</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settlement-inline-grid">
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">操作用户 ID *</label>
|
||||
<input v-model.trim="form.userid" class="settlement-input" type="text" placeholder="请输入当前操作用户 ID">
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">审批业务名</label>
|
||||
<input v-model.trim="form.business_name" class="settlement-input" type="text" placeholder="财务结算">
|
||||
<small style="display: block; margin-top: 6px; color: #94a3b8; font-size: 11px;">需要在 apv_business 表中存在该业务配置</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="settlement-alert settlement-alert--warning" style="margin: 20px 0;">
|
||||
<span class="settlement-alert-icon"><i class="el-icon-warning-outline" /></span>
|
||||
<div class="settlement-alert-content">
|
||||
<b>提交前请确认结算数据</b>
|
||||
<p>提交后结算单状态将变为审批中,等待审批系统回调结果。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="result" class="settlement-alert" style="margin: 0 0 20px;">
|
||||
<span class="settlement-alert-icon"><i class="el-icon-circle-check" /></span>
|
||||
<div class="settlement-alert-content">
|
||||
<b>提交成功</b>
|
||||
<p>审批 ID:{{ result.approvalId || '-' }},当前状态:<SettlementStatusBadge :status="result.status" /></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button type="button" class="settlement-btn settlement-btn--primary" :disabled="submitting" @click="submitApproval">
|
||||
<i class="el-icon-s-promotion" />{{ submitting ? '提交中...' : '提交审批' }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<section v-else class="settlement-card settlement-card--padded" style="color: #94a3b8; text-align: center;">
|
||||
{{ loadingList ? '正在加载待提交结算单...' : '暂无可提交审批的结算单' }}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</SettlementPageFrame>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettlementPageFrame from './SettlementPageFrame'
|
||||
import SettlementPageHeader from './SettlementPageHeader'
|
||||
import SettlementMetricCard from './SettlementMetricCard'
|
||||
import SettlementStatusBadge from './SettlementStatusBadge'
|
||||
import { settlementListAPI, submitApprovalAPI } from '@/api/FinancialSettlementCenter'
|
||||
import {
|
||||
getAccountingOrgId,
|
||||
getCurrentUserId,
|
||||
getItems,
|
||||
getPayload,
|
||||
isSubmittable,
|
||||
normalizeSettlement
|
||||
} from './settlementHelpers'
|
||||
|
||||
export default {
|
||||
name: 'SubmitforApproval',
|
||||
components: {
|
||||
SettlementPageFrame,
|
||||
SettlementPageHeader,
|
||||
SettlementMetricCard,
|
||||
SettlementStatusBadge
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loadingList: false,
|
||||
submitting: false,
|
||||
submitRows: [],
|
||||
selectedRow: null,
|
||||
result: null,
|
||||
form: {
|
||||
userid: getCurrentUserId(),
|
||||
business_name: '财务结算'
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadList()
|
||||
},
|
||||
methods: {
|
||||
async loadList() {
|
||||
const accountingOrgid = getAccountingOrgId(this.$route)
|
||||
if (!accountingOrgid) {
|
||||
this.$message.warning('未获取到核算机构 ID')
|
||||
return
|
||||
}
|
||||
|
||||
this.loadingList = true
|
||||
try {
|
||||
const payload = getPayload(await settlementListAPI({
|
||||
accounting_orgid: accountingOrgid,
|
||||
current_page: 1,
|
||||
page_size: 100
|
||||
}))
|
||||
this.submitRows = getItems(payload).map(normalizeSettlement).filter(row => isSubmittable(row.status))
|
||||
const routeId = this.$route.query.id
|
||||
this.selectedRow = this.submitRows.find(row => row.id === routeId) || this.submitRows[0] || null
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '待提交结算单查询失败')
|
||||
} finally {
|
||||
this.loadingList = false
|
||||
}
|
||||
},
|
||||
selectRow(row) {
|
||||
this.selectedRow = row
|
||||
this.result = null
|
||||
this.$router.replace({
|
||||
name: 'FinancialSubmitforApproval',
|
||||
query: { id: row.id }
|
||||
})
|
||||
},
|
||||
async submitApproval() {
|
||||
if (!this.selectedRow) return
|
||||
if (!this.form.userid || !this.form.business_name) {
|
||||
this.$message.warning('请填写操作用户 ID 和审批业务名')
|
||||
return
|
||||
}
|
||||
|
||||
this.submitting = true
|
||||
try {
|
||||
const payload = getPayload(await submitApprovalAPI({
|
||||
settlement_id: this.selectedRow.id,
|
||||
userid: this.form.userid,
|
||||
business_name: this.form.business_name
|
||||
}))
|
||||
this.result = normalizeSettlement(payload.settlement || payload)
|
||||
this.result.status = this.result.status === 'draft' ? 'approving' : this.result.status
|
||||
this.$message.success('审批提交成功')
|
||||
await this.loadList()
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '提交审批失败')
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
241
f/web-kboss/src/views/FinancialSettlementCenter/SummaryQuery.vue
Normal file
241
f/web-kboss/src/views/FinancialSettlementCenter/SummaryQuery.vue
Normal file
@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<SettlementPageFrame>
|
||||
<SettlementPageHeader
|
||||
icon="el-icon-s-data"
|
||||
title="汇总查询"
|
||||
description="查询供应商或分销商在指定账期内的销售金额、结算金额、平台收入和账单数量。"
|
||||
endpoint="GET /bill/finance_settlement_summary.dspy"
|
||||
>
|
||||
<template #actions>
|
||||
<button type="button" class="settlement-btn" :disabled="loading" @click="resetQuery">
|
||||
<i class="el-icon-refresh" />重置
|
||||
</button>
|
||||
<button type="button" class="settlement-btn settlement-btn--primary" :disabled="loading" @click="search">
|
||||
<i class="el-icon-search" />{{ loading ? '查询中...' : '查询' }}
|
||||
</button>
|
||||
</template>
|
||||
</SettlementPageHeader>
|
||||
|
||||
<section class="settlement-card settlement-card--padded" style="margin-bottom: 24px;">
|
||||
<div class="settlement-card-heading" style="margin-bottom: 18px;">
|
||||
<i class="el-icon-filter" />
|
||||
<span>查询条件</span>
|
||||
</div>
|
||||
<div class="settlement-filter-grid">
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">对手方类型</label>
|
||||
<div class="settlement-choice-group">
|
||||
<button type="button" class="settlement-choice" :class="{ 'is-active': form.counterparty_type === 'supplier' }" @click="setCounterpartyType('supplier')">供应商</button>
|
||||
<button type="button" class="settlement-choice" :class="{ 'is-active': form.counterparty_type === 'reseller' }" @click="setCounterpartyType('reseller')">分销商</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">账期类型</label>
|
||||
<div class="settlement-choice-group">
|
||||
<button type="button" class="settlement-choice" :class="{ 'is-active': form.period_type === 'day' }" @click="setPeriodType('day')">日结</button>
|
||||
<button type="button" class="settlement-choice" :class="{ 'is-active': form.period_type === 'month' }" @click="setPeriodType('month')">月结</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">开始日期</label>
|
||||
<input v-model="form.start_date" class="settlement-input" type="date">
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">结束日期</label>
|
||||
<input v-model="form.end_date" class="settlement-input" type="date">
|
||||
</div>
|
||||
<div class="settlement-field">
|
||||
<label class="settlement-label">指定对手方</label>
|
||||
<input v-model.trim="form.counterparty_orgid" class="settlement-input" type="text" placeholder="可选:对手方机构 ID">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="settlement-metric-grid">
|
||||
<SettlementMetricCard label="销售金额合计" :value="summary.sales" hint="汇总销售总额" icon="el-icon-wallet" />
|
||||
<SettlementMetricCard label="结算金额合计" :value="summary.settlement" hint="应结算给对手方" variant="emerald" icon="el-icon-s-order" />
|
||||
<SettlementMetricCard label="平台收入合计" :value="summary.income" hint="折扣收入 + 底价收入" variant="amber" icon="el-icon-s-marketing" />
|
||||
<SettlementMetricCard label="账单数量" :value="summary.billCount" suffix="笔" hint="已记账账单" variant="violet" icon="el-icon-document" />
|
||||
</div>
|
||||
|
||||
<section class="settlement-card">
|
||||
<div class="settlement-card-head">
|
||||
<div class="settlement-card-heading">
|
||||
<i class="el-icon-office-building" />
|
||||
<span>汇总明细</span>
|
||||
<small class="settlement-card-subtitle">· 共 {{ total }} 条</small>
|
||||
</div>
|
||||
<button type="button" class="settlement-btn settlement-btn--ghost" disabled>
|
||||
<i class="el-icon-download" />导出
|
||||
</button>
|
||||
</div>
|
||||
<div class="settlement-table-wrap">
|
||||
<table class="settlement-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>账期</th>
|
||||
<th>对手方 ID</th>
|
||||
<th>对手方名称</th>
|
||||
<th class="is-number">销售金额</th>
|
||||
<th class="is-number">结算金额</th>
|
||||
<th class="is-number">平台收入</th>
|
||||
<th class="is-number">账单数</th>
|
||||
<th class="is-number">收入占比</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in summaryRows" :key="row.id">
|
||||
<td><span class="is-mono"><i class="el-icon-date" style="margin-right: 5px; color: #94a3b8;" />{{ row.period }}</span></td>
|
||||
<td class="is-mono is-muted">{{ row.id }}</td>
|
||||
<td style="font-weight: 600;">{{ row.name }}</td>
|
||||
<td class="is-number">{{ row.sales }}</td>
|
||||
<td class="is-number is-emerald">{{ row.settlement }}</td>
|
||||
<td class="is-number is-amber">{{ row.income }}</td>
|
||||
<td class="is-number is-muted">{{ row.bills }}</td>
|
||||
<td class="is-number">
|
||||
<span class="settlement-contribution">
|
||||
<span class="settlement-contribution-bar"><span :style="{ width: row.ratio }" /></span>
|
||||
<small style="font-size: 11px; color: #64748b;">{{ row.ratio }}</small>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!loading && !summaryRows.length">
|
||||
<td colspan="8" style="padding: 42px; color: #94a3b8; text-align: center;">暂无查询数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="settlement-table-footer">
|
||||
<span>第 {{ form.current_page }} 页 · 每页 {{ form.page_size }} 条 · 共 {{ total }} 条</span>
|
||||
<div class="settlement-pagination">
|
||||
<button type="button" class="settlement-btn" :disabled="form.current_page <= 1 || loading" @click="changePage(-1)"><i class="el-icon-arrow-left" style="margin: 0;" /></button>
|
||||
<span class="settlement-page-number">{{ form.current_page }} / {{ totalPages }}</span>
|
||||
<button type="button" class="settlement-btn" :disabled="form.current_page >= totalPages || loading" @click="changePage(1)"><i class="el-icon-arrow-right" style="margin: 0;" /></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</SettlementPageFrame>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettlementPageFrame from './SettlementPageFrame'
|
||||
import SettlementPageHeader from './SettlementPageHeader'
|
||||
import SettlementMetricCard from './SettlementMetricCard'
|
||||
import { summaryQueryAPI } from '@/api/FinancialSettlementCenter'
|
||||
import {
|
||||
getAccountingOrgId,
|
||||
getCurrentPage,
|
||||
getItems,
|
||||
getPageSize,
|
||||
getPayload,
|
||||
getSummary,
|
||||
getTotal,
|
||||
normalizeSummaryItem,
|
||||
percent
|
||||
} from './settlementHelpers'
|
||||
|
||||
export default {
|
||||
name: 'SummaryQuery',
|
||||
components: {
|
||||
SettlementPageFrame,
|
||||
SettlementPageHeader,
|
||||
SettlementMetricCard
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
total: 0,
|
||||
summaryRows: [],
|
||||
summary: {
|
||||
sales: '¥0.00',
|
||||
settlement: '¥0.00',
|
||||
income: '¥0.00',
|
||||
billCount: 0
|
||||
},
|
||||
form: {
|
||||
counterparty_type: 'supplier',
|
||||
period_type: 'month',
|
||||
start_date: '2026-03-01',
|
||||
end_date: '2026-06-30',
|
||||
counterparty_orgid: '',
|
||||
current_page: 1,
|
||||
page_size: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalPages() {
|
||||
return Math.max(1, Math.ceil(this.total / this.form.page_size))
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
getParams() {
|
||||
return {
|
||||
accounting_orgid: getAccountingOrgId(this.$route),
|
||||
counterparty_type: this.form.counterparty_type,
|
||||
period_type: this.form.period_type,
|
||||
start_date: this.form.start_date,
|
||||
end_date: this.form.end_date,
|
||||
counterparty_orgid: this.form.counterparty_orgid || undefined,
|
||||
current_page: this.form.current_page,
|
||||
page_size: this.form.page_size
|
||||
}
|
||||
},
|
||||
async loadData() {
|
||||
const params = this.getParams()
|
||||
if (!params.accounting_orgid) {
|
||||
this.$message.warning('未获取到核算机构 ID')
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const payload = getPayload(await summaryQueryAPI(params))
|
||||
this.summary = getSummary(payload)
|
||||
this.summaryRows = getItems(payload).map(normalizeSummaryItem).map(item => ({
|
||||
...item,
|
||||
ratio: percent(item.salesAmount, this.summary.salesAmount)
|
||||
}))
|
||||
this.total = getTotal(payload, this.summaryRows.length)
|
||||
this.form.current_page = getCurrentPage(payload, this.form.current_page)
|
||||
this.form.page_size = getPageSize(payload, this.form.page_size)
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '汇总查询失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
search() {
|
||||
this.form.current_page = 1
|
||||
this.loadData()
|
||||
},
|
||||
resetQuery() {
|
||||
this.form = {
|
||||
counterparty_type: 'supplier',
|
||||
period_type: 'month',
|
||||
start_date: '2026-03-01',
|
||||
end_date: '2026-06-30',
|
||||
counterparty_orgid: '',
|
||||
current_page: 1,
|
||||
page_size: 20
|
||||
}
|
||||
this.loadData()
|
||||
},
|
||||
setCounterpartyType(type) {
|
||||
this.form.counterparty_type = type
|
||||
},
|
||||
setPeriodType(type) {
|
||||
this.form.period_type = type
|
||||
},
|
||||
changePage(offset) {
|
||||
const nextPage = this.form.current_page + offset
|
||||
if (nextPage < 1 || nextPage > this.totalPages) return
|
||||
this.form.current_page = nextPage
|
||||
this.loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,215 @@
|
||||
const TYPE_LABELS = {
|
||||
supplier: '供应商',
|
||||
reseller: '分销商'
|
||||
}
|
||||
|
||||
const PERIOD_LABELS = {
|
||||
day: '日结',
|
||||
month: '月结'
|
||||
}
|
||||
|
||||
const SALE_MODE_LABELS = {
|
||||
0: '折扣销售',
|
||||
1: '代理销售',
|
||||
2: '底价销售'
|
||||
}
|
||||
|
||||
const STATUS_ALIASES = {
|
||||
start: 'approving',
|
||||
agree: 'approved',
|
||||
refuse: 'rejected',
|
||||
terminate: 'cancelled',
|
||||
approve: 'approved',
|
||||
cancel: 'cancelled'
|
||||
}
|
||||
|
||||
export function getPayload(response) {
|
||||
if (!response) return {}
|
||||
if (response.status === false) {
|
||||
throw new Error(response.msg || response.message || '请求失败')
|
||||
}
|
||||
return response.data !== undefined && response.data !== null ? response.data : response
|
||||
}
|
||||
|
||||
export function getItems(payload) {
|
||||
if (Array.isArray(payload)) return payload
|
||||
if (!payload || typeof payload !== 'object') return []
|
||||
|
||||
const keys = ['items', 'list', 'records', 'rows', 'details', 'detail_items', 'result_list', 'detail_list', 'data_list']
|
||||
for (let index = 0; index < keys.length; index += 1) {
|
||||
const value = payload[keys[index]]
|
||||
if (Array.isArray(value)) return value
|
||||
}
|
||||
if (payload.data && typeof payload.data === 'object') return getItems(payload.data)
|
||||
return []
|
||||
}
|
||||
|
||||
export function getTotal(payload, fallback = 0) {
|
||||
if (!payload || typeof payload !== 'object') return fallback
|
||||
const value = payload.total_count || payload.total || payload.count || payload.totalCount
|
||||
if ((value === undefined || value === null) && payload.data && typeof payload.data === 'object') {
|
||||
return getTotal(payload.data, fallback)
|
||||
}
|
||||
return Number(value === undefined || value === null ? fallback : value)
|
||||
}
|
||||
|
||||
export function getCurrentPage(payload, fallback = 1) {
|
||||
if (!payload || typeof payload !== 'object') return fallback
|
||||
const value = payload.current_page || payload.page || payload.currentPage
|
||||
if ((value === undefined || value === null) && payload.data && typeof payload.data === 'object') {
|
||||
return getCurrentPage(payload.data, fallback)
|
||||
}
|
||||
return Number(value === undefined || value === null ? fallback : value)
|
||||
}
|
||||
|
||||
export function getPageSize(payload, fallback = 20) {
|
||||
if (!payload || typeof payload !== 'object') return fallback
|
||||
const value = payload.page_size || payload.limit || payload.pageSize
|
||||
if ((value === undefined || value === null) && payload.data && typeof payload.data === 'object') {
|
||||
return getPageSize(payload.data, fallback)
|
||||
}
|
||||
return Number(value === undefined || value === null ? fallback : value)
|
||||
}
|
||||
|
||||
export function getAccountingOrgId(route) {
|
||||
return (route && route.query && route.query.orgid) ||
|
||||
sessionStorage.getItem('orgid') ||
|
||||
localStorage.getItem('orgid') ||
|
||||
''
|
||||
}
|
||||
|
||||
export function getCurrentUserId() {
|
||||
return sessionStorage.getItem('userId') ||
|
||||
sessionStorage.getItem('userid') ||
|
||||
sessionStorage.getItem('username') ||
|
||||
''
|
||||
}
|
||||
|
||||
export function numberValue(value) {
|
||||
if (value === undefined || value === null || value === '') return 0
|
||||
if (typeof value === 'number') return Number.isFinite(value) ? value : 0
|
||||
const numeric = Number(String(value).replace(/[^\d.-]/g, ''))
|
||||
return Number.isFinite(numeric) ? numeric : 0
|
||||
}
|
||||
|
||||
export function money(value) {
|
||||
return `¥${numberValue(value).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})}`
|
||||
}
|
||||
|
||||
export function percent(value, total) {
|
||||
const ratio = numberValue(total) ? (numberValue(value) / numberValue(total)) * 100 : 0
|
||||
return `${ratio.toFixed(1)}%`
|
||||
}
|
||||
|
||||
export function normalizeStatus(value) {
|
||||
const status = String(value || 'draft').toLowerCase()
|
||||
return STATUS_ALIASES[status] || status
|
||||
}
|
||||
|
||||
function read(source, keys, fallback = '') {
|
||||
if (!source || typeof source !== 'object') return fallback
|
||||
for (let index = 0; index < keys.length; index += 1) {
|
||||
const value = source[keys[index]]
|
||||
if (value !== undefined && value !== null && value !== '') return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
export function normalizeSettlement(item = {}) {
|
||||
const counterparty = item.counterparty || {}
|
||||
const typeKey = String(read(item, ['counterparty_type', 'type'], read(counterparty, ['type'], ''))).toLowerCase()
|
||||
const periodKey = String(read(item, ['period_type'], '')).toLowerCase()
|
||||
const salesAmount = numberValue(read(item, ['sales_amount', 'sales_total', 'sale_amount']))
|
||||
const settlementAmount = numberValue(read(item, ['settlement_amount', 'settle_amount', 'settle_upstream_amount']))
|
||||
const incomeAmount = numberValue(read(item, ['platform_income_amount', 'profit_amount', 'platform_income']))
|
||||
|
||||
return {
|
||||
id: String(read(item, ['settlement_id', 'id', 'settlementId'])),
|
||||
no: read(item, ['settlement_no', 'settlement_number', 'no'], '-'),
|
||||
typeKey,
|
||||
type: TYPE_LABELS[typeKey] || read(item, ['counterparty_type_name', 'type_name'], read(counterparty, ['type_name'], '-')),
|
||||
orgId: read(item, ['counterparty_orgid', 'counterparty_id', 'orgid'], read(counterparty, ['orgid', 'id'], '-')),
|
||||
name: read(item, ['counterparty_name', 'name'], read(counterparty, ['name'], '-')),
|
||||
periodTypeKey: periodKey,
|
||||
periodType: PERIOD_LABELS[periodKey] || read(item, ['period_type_name'], '-'),
|
||||
start: read(item, ['period_start', 'start_date', 'start'], '-'),
|
||||
end: read(item, ['period_end', 'end_date', 'end'], '-'),
|
||||
salesAmount,
|
||||
settlementAmount,
|
||||
incomeAmount,
|
||||
sales: money(salesAmount),
|
||||
settlement: money(settlementAmount),
|
||||
income: money(incomeAmount),
|
||||
status: normalizeStatus(read(item, ['status', 'settlement_status'], 'draft')),
|
||||
createdAt: read(item, ['created_at', 'create_time', 'createdAt'], '-'),
|
||||
approvalId: read(item, ['approval_id', 'apv_id', 'approvalId'], ''),
|
||||
reason: read(item, ['failure_reason', 'reason', 'fail_reason'], '')
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeSummaryItem(item = {}) {
|
||||
const salesAmount = numberValue(read(item, ['sales_amount', 'sales_total', 'sale_amount']))
|
||||
const settlementAmount = numberValue(read(item, ['settlement_amount', 'settle_amount', 'settle_upstream_amount']))
|
||||
const incomeAmount = numberValue(read(item, ['platform_income_amount', 'profit_amount', 'platform_income']))
|
||||
|
||||
return {
|
||||
period: read(item, ['period', 'period_name', 'settlement_period'], '-'),
|
||||
id: read(item, ['counterparty_orgid', 'counterparty_id', 'orgid'], '-'),
|
||||
name: read(item, ['counterparty_name', 'name'], '-'),
|
||||
typeKey: String(read(item, ['counterparty_type', 'type'], '')).toLowerCase(),
|
||||
salesAmount,
|
||||
settlementAmount,
|
||||
incomeAmount,
|
||||
sales: money(salesAmount),
|
||||
settlement: money(settlementAmount),
|
||||
income: money(incomeAmount),
|
||||
bills: numberValue(read(item, ['bill_count', 'count'])),
|
||||
ratio: '0%'
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeBill(item = {}) {
|
||||
const salesAmount = numberValue(read(item, ['sales_amount', 'sales_total', 'sale_amount']))
|
||||
const settlementAmount = numberValue(read(item, ['settlement_amount', 'settle_amount', 'settle_upstream_amount']))
|
||||
const incomeAmount = numberValue(read(item, ['platform_income_amount', 'profit_amount', 'platform_income']))
|
||||
const mode = read(item, ['sale_mode', 'sales_mode', 'mode'], '')
|
||||
|
||||
return {
|
||||
billId: read(item, ['bill_id', 'id', 'billId'], '-'),
|
||||
orderId: read(item, ['order_id', 'order_no', 'orderId'], '-'),
|
||||
date: read(item, ['bill_date', 'date', 'created_at'], '-'),
|
||||
mode: SALE_MODE_LABELS[mode] || mode || '-',
|
||||
salesAmount,
|
||||
settlementAmount,
|
||||
incomeAmount,
|
||||
sales: money(salesAmount),
|
||||
settlement: money(settlementAmount),
|
||||
income: money(incomeAmount)
|
||||
}
|
||||
}
|
||||
|
||||
export function getSummary(payload = {}) {
|
||||
const nested = payload.data && typeof payload.data === 'object' ? payload.data : {}
|
||||
const summary = payload.summary || payload.total || payload.totals || nested.summary || nested.total || nested.totals || payload
|
||||
const salesAmount = numberValue(read(summary, ['sales_amount', 'sales_total', 'sale_amount']))
|
||||
const settlementAmount = numberValue(read(summary, ['settlement_amount', 'settle_amount', 'settle_upstream_amount']))
|
||||
const incomeAmount = numberValue(read(summary, ['platform_income_amount', 'profit_amount', 'platform_income']))
|
||||
const billCount = numberValue(read(summary, ['bill_count', 'count']))
|
||||
|
||||
return {
|
||||
salesAmount,
|
||||
settlementAmount,
|
||||
incomeAmount,
|
||||
billCount,
|
||||
sales: money(salesAmount),
|
||||
settlement: money(settlementAmount),
|
||||
income: money(incomeAmount)
|
||||
}
|
||||
}
|
||||
|
||||
export function isSubmittable(status) {
|
||||
return ['draft', 'rejected', 'failed'].includes(normalizeStatus(status))
|
||||
}
|
||||
@ -0,0 +1,247 @@
|
||||
export const settlementRows = [
|
||||
{
|
||||
id: 'stl20260601001',
|
||||
no: 'STL202606010001',
|
||||
type: '供应商',
|
||||
typeKey: 'supplier',
|
||||
orgId: 'supplier001',
|
||||
name: '云海科技股份有限公司',
|
||||
periodType: '月结',
|
||||
start: '2026-05-01',
|
||||
end: '2026-05-31',
|
||||
sales: '¥156,800.00',
|
||||
settlement: '¥109,760.00',
|
||||
income: '¥47,040.00',
|
||||
status: 'settled',
|
||||
createdAt: '2026-06-01 09:23:15',
|
||||
approvalId: 'apv20260601001'
|
||||
},
|
||||
{
|
||||
id: 'stl20260602001',
|
||||
no: 'STL202606020002',
|
||||
type: '供应商',
|
||||
typeKey: 'supplier',
|
||||
orgId: 'supplier002',
|
||||
name: '星际云计算服务公司',
|
||||
periodType: '月结',
|
||||
start: '2026-05-01',
|
||||
end: '2026-05-31',
|
||||
sales: '¥234,500.00',
|
||||
settlement: '¥164,150.00',
|
||||
income: '¥70,350.00',
|
||||
status: 'approved',
|
||||
createdAt: '2026-06-02 10:45:30',
|
||||
approvalId: 'apv20260602001'
|
||||
},
|
||||
{
|
||||
id: 'stl20260603001',
|
||||
no: 'STL202606030003',
|
||||
type: '分销商',
|
||||
typeKey: 'reseller',
|
||||
orgId: 'reseller001',
|
||||
name: '东方明珠销售有限公司',
|
||||
periodType: '月结',
|
||||
start: '2026-05-01',
|
||||
end: '2026-05-31',
|
||||
sales: '¥89,500.00',
|
||||
settlement: '¥76,075.00',
|
||||
income: '¥13,425.00',
|
||||
status: 'approving',
|
||||
createdAt: '2026-06-03 14:20:00',
|
||||
approvalId: 'apv20260603001'
|
||||
},
|
||||
{
|
||||
id: 'stl20260604001',
|
||||
no: 'STL202606040004',
|
||||
type: '供应商',
|
||||
typeKey: 'supplier',
|
||||
orgId: 'supplier003',
|
||||
name: '智算数据科技有限公司',
|
||||
periodType: '月结',
|
||||
start: '2026-05-01',
|
||||
end: '2026-05-31',
|
||||
sales: '¥178,900.00',
|
||||
settlement: '¥125,230.00',
|
||||
income: '¥53,670.00',
|
||||
status: 'draft',
|
||||
createdAt: '2026-06-04 11:15:45',
|
||||
approvalId: ''
|
||||
},
|
||||
{
|
||||
id: 'stl20260605001',
|
||||
no: 'STL202606050005',
|
||||
type: '分销商',
|
||||
typeKey: 'reseller',
|
||||
orgId: 'reseller002',
|
||||
name: '粤港澳渠道服务商',
|
||||
periodType: '月结',
|
||||
start: '2026-05-01',
|
||||
end: '2026-05-31',
|
||||
sales: '¥123,400.00',
|
||||
settlement: '¥104,890.00',
|
||||
income: '¥18,510.00',
|
||||
status: 'rejected',
|
||||
createdAt: '2026-06-05 16:30:20',
|
||||
approvalId: 'apv20260605001',
|
||||
reason: '金额与对账单不一致,需重新核对'
|
||||
},
|
||||
{
|
||||
id: 'stl20260606001',
|
||||
no: 'STL202606060006',
|
||||
type: '供应商',
|
||||
typeKey: 'supplier',
|
||||
orgId: 'supplier004',
|
||||
name: '极光网络通信集团',
|
||||
periodType: '月结',
|
||||
start: '2026-05-01',
|
||||
end: '2026-05-31',
|
||||
sales: '¥267,800.00',
|
||||
settlement: '¥187,460.00',
|
||||
income: '¥80,340.00',
|
||||
status: 'failed',
|
||||
createdAt: '2026-06-06 08:50:10',
|
||||
approvalId: 'apv20260606001',
|
||||
reason: '银行账户信息错误,结算转账失败'
|
||||
}
|
||||
]
|
||||
|
||||
export const summaryRows = [
|
||||
{
|
||||
period: '2026-06',
|
||||
id: 'supplier001',
|
||||
name: '云海科技股份有限公司',
|
||||
sales: '¥156,800.00',
|
||||
settlement: '¥109,760.00',
|
||||
income: '¥47,040.00',
|
||||
bills: '42',
|
||||
ratio: '22.6%'
|
||||
},
|
||||
{
|
||||
period: '2026-06',
|
||||
id: 'supplier002',
|
||||
name: '星际云计算服务公司',
|
||||
sales: '¥142,600.00',
|
||||
settlement: '¥99,820.00',
|
||||
income: '¥42,780.00',
|
||||
bills: '36',
|
||||
ratio: '20.6%'
|
||||
},
|
||||
{
|
||||
period: '2026-06',
|
||||
id: 'supplier003',
|
||||
name: '智算数据科技有限公司',
|
||||
sales: '¥98,760.00',
|
||||
settlement: '¥69,132.00',
|
||||
income: '¥29,628.00',
|
||||
bills: '27',
|
||||
ratio: '14.2%'
|
||||
},
|
||||
{
|
||||
period: '2026-06',
|
||||
id: 'reseller001',
|
||||
name: '东方明珠销售有限公司',
|
||||
sales: '¥86,400.00',
|
||||
settlement: '¥73,440.00',
|
||||
income: '¥12,960.00',
|
||||
bills: '19',
|
||||
ratio: '12.5%'
|
||||
}
|
||||
]
|
||||
|
||||
export const billRows = [
|
||||
{
|
||||
billId: 'B202606010001',
|
||||
orderId: 'O202606001',
|
||||
date: '2026-06-01',
|
||||
mode: '标准销售',
|
||||
sales: '¥12,580.00',
|
||||
settlement: '¥8,806.00',
|
||||
income: '¥3,774.00'
|
||||
},
|
||||
{
|
||||
billId: 'B202606010002',
|
||||
orderId: 'O202606002',
|
||||
date: '2026-06-03',
|
||||
mode: '代理销售',
|
||||
sales: '¥8,920.00',
|
||||
settlement: '¥6,244.00',
|
||||
income: '¥2,676.00'
|
||||
},
|
||||
{
|
||||
billId: 'B202606010003',
|
||||
orderId: 'O202606003',
|
||||
date: '2026-06-08',
|
||||
mode: '标准销售',
|
||||
sales: '¥15,600.00',
|
||||
settlement: '¥10,920.00',
|
||||
income: '¥4,680.00'
|
||||
},
|
||||
{
|
||||
billId: 'B202606010004',
|
||||
orderId: 'O202606004',
|
||||
date: '2026-06-12',
|
||||
mode: '项目销售',
|
||||
sales: '¥19,860.00',
|
||||
settlement: '¥13,902.00',
|
||||
income: '¥5,958.00'
|
||||
},
|
||||
{
|
||||
billId: 'B202606010005',
|
||||
orderId: 'O202606005',
|
||||
date: '2026-06-21',
|
||||
mode: '标准销售',
|
||||
sales: '¥11,720.00',
|
||||
settlement: '¥8,204.00',
|
||||
income: '¥3,516.00'
|
||||
}
|
||||
]
|
||||
|
||||
export const flowSteps = [
|
||||
{
|
||||
index: 1,
|
||||
icon: 'el-icon-s-data',
|
||||
title: '汇总查询',
|
||||
subtitle: '数据盘点',
|
||||
description: '查询指定账期内供应商或分销商的销售额、结算金额、平台收入和账单数量。',
|
||||
endpoint: '/bill/finance_settlement_summary.dspy'
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
icon: 'el-icon-view',
|
||||
title: '结算单预览',
|
||||
subtitle: '核对明细',
|
||||
description: '创建前预览结算明细,检查指定账期是否已存在结算单,避免重复创建。',
|
||||
endpoint: '/bill/finance_settlement_preview.dspy'
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
icon: 'el-icon-circle-plus-outline',
|
||||
title: '创建结算单',
|
||||
subtitle: '生成草稿',
|
||||
description: '基于预览数据创建结算单主表和明细快照,结算单初始状态为草稿。',
|
||||
endpoint: '/bill/finance_settlement_create.dspy'
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
icon: 'el-icon-s-promotion',
|
||||
title: '提交审批',
|
||||
subtitle: '送审',
|
||||
description: '将草稿、审批拒绝或结算失败状态的结算单提交至审批流程。',
|
||||
endpoint: '/bill/finance_settlement_submit.dspy'
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
icon: 'el-icon-circle-check',
|
||||
title: '审批回调',
|
||||
subtitle: '记账闭环',
|
||||
description: '接收审批结果,审批通过后自动完成结算记账并更新结算单状态。',
|
||||
endpoint: '/bill/finance_settlement_apv_callback.dspy'
|
||||
}
|
||||
]
|
||||
|
||||
export const incomeRanks = [
|
||||
{ name: '星际云计算服务公司', type: '供应商', income: '¥70,350.00', ratio: '26.3%', width: '100%' },
|
||||
{ name: '极光网络通信集团', type: '供应商', income: '¥62,480.00', ratio: '23.4%', width: '89%' },
|
||||
{ name: '云海科技股份有限公司', type: '供应商', income: '¥47,040.00', ratio: '17.6%', width: '67%' },
|
||||
{ name: '东方明珠销售有限公司', type: '分销商', income: '¥31,860.00', ratio: '11.9%', width: '45%' }
|
||||
]
|
||||
@ -63,7 +63,7 @@
|
||||
<el-form-item label="5. 请选择您的企业类型(单选)" prop="enterprise_type">
|
||||
<el-radio-group v-model="formData.enterprise_type" class="option-grid">
|
||||
<el-radio
|
||||
v-for="item in enterpriseOptions"
|
||||
v-for="item in localizedEnterpriseOptions"
|
||||
:key="item.id"
|
||||
:label="String(item.id)"
|
||||
class="option-item"
|
||||
@ -82,7 +82,7 @@
|
||||
class="full-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in provinceOptions"
|
||||
v-for="item in localizedProvinceOptions"
|
||||
:key="`region-${item.id}-${item.name}`"
|
||||
:label="item.name"
|
||||
:value="String(item.id)"
|
||||
@ -93,7 +93,7 @@
|
||||
<el-form-item label="7. 您想咨询的方向(可多选)">
|
||||
<el-checkbox-group v-model="formData.consult_direction_list" class="direction-list">
|
||||
<el-checkbox
|
||||
v-for="item in consultDirectionOptions"
|
||||
v-for="item in localizedConsultDirectionOptions"
|
||||
:key="item.id"
|
||||
:label="item.id"
|
||||
class="direction-item"
|
||||
@ -141,6 +141,14 @@
|
||||
|
||||
<script>
|
||||
import { reqConsultForm, reqProductConsult } from '@/api/H5'
|
||||
import {
|
||||
extractConsultOptionList,
|
||||
isEnglishLocale,
|
||||
localizeDictOptions,
|
||||
localizeDirectionOptions,
|
||||
normalizeDictOptions as buildDictOptions,
|
||||
normalizeDirectionOptions as buildDirectionOptions
|
||||
} from '@/utils/consultDict'
|
||||
|
||||
export default {
|
||||
name: 'MobileFullScreenConsultDialog',
|
||||
@ -207,9 +215,21 @@ export default {
|
||||
set(value) {
|
||||
this.$emit('update:visible', value)
|
||||
}
|
||||
},
|
||||
localizedEnterpriseOptions() {
|
||||
return localizeDictOptions(this.enterpriseOptions, isEnglishLocale(this.$i18n))
|
||||
},
|
||||
localizedProvinceOptions() {
|
||||
return localizeDictOptions(this.provinceOptions, isEnglishLocale(this.$i18n))
|
||||
},
|
||||
localizedConsultDirectionOptions() {
|
||||
return localizeDirectionOptions(this.consultDirectionOptions, isEnglishLocale(this.$i18n))
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$i18n.locale'() {
|
||||
this.provinceSelectKey += 1
|
||||
},
|
||||
visible: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
@ -256,44 +276,15 @@ export default {
|
||||
this.dialogVisible = false
|
||||
this.$emit('close')
|
||||
},
|
||||
normalizeDictOptions(list, type) {
|
||||
return list
|
||||
.filter(item => item && item.dict_type === type)
|
||||
.sort((a, b) => Number(a.sort_order || 0) - Number(b.sort_order || 0))
|
||||
.map(item => ({
|
||||
id: String(item.dict_key),
|
||||
name: item.dict_value
|
||||
}))
|
||||
},
|
||||
normalizeDirectionOptions(list) {
|
||||
return list
|
||||
.filter(item => item && item.dict_type === 'direction')
|
||||
.sort((a, b) => Number(a.sort_order || 0) - Number(b.sort_order || 0))
|
||||
.map(item => {
|
||||
const value = item.dict_value || ''
|
||||
const match = value.match(/^(.+?)((.+?))$/)
|
||||
return {
|
||||
id: String(item.dict_key),
|
||||
name: value,
|
||||
title: match ? match[1] : value,
|
||||
desc: match ? match[2] : ''
|
||||
}
|
||||
})
|
||||
},
|
||||
extractConsultOptionList(res) {
|
||||
if (res && Array.isArray(res.data)) return res.data
|
||||
if (res && res.data && Array.isArray(res.data.data)) return res.data.data
|
||||
return []
|
||||
},
|
||||
async fetchConsultOptions() {
|
||||
try {
|
||||
const res = await reqConsultForm()
|
||||
const list = this.extractConsultOptionList(res)
|
||||
const list = extractConsultOptionList(res)
|
||||
if (!list.length) return
|
||||
|
||||
const enterpriseOptions = this.normalizeDictOptions(list, 'enterprise_type')
|
||||
const provinceOptions = this.normalizeDictOptions(list, 'region')
|
||||
const consultDirectionOptions = this.normalizeDirectionOptions(list)
|
||||
const enterpriseOptions = buildDictOptions(list, 'enterprise_type')
|
||||
const provinceOptions = buildDictOptions(list, 'region')
|
||||
const consultDirectionOptions = buildDirectionOptions(list)
|
||||
|
||||
if (enterpriseOptions.length) this.enterpriseOptions = enterpriseOptions
|
||||
if (provinceOptions.length) {
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
>
|
||||
<div class="login-card">
|
||||
<div class="brand-area">
|
||||
|
||||
|
||||
<div class="brand-text">
|
||||
<h2>{{ activeTab === 'login' ? '欢迎登录' : '创建账号' }}</h2>
|
||||
</div>
|
||||
@ -41,7 +41,7 @@
|
||||
prefix-icon="el-icon-lock"
|
||||
@keyup.enter.native="handleLogin"
|
||||
>
|
||||
<i slot="suffix" class="el-icon-view password-eye" @click="loginPwdVisible = !loginPwdVisible"></i>
|
||||
<i slot="suffix" class="el-icon-view password-eye" @click="loginPwdVisible = !loginPwdVisible" />
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
@ -93,14 +93,18 @@
|
||||
prefix-icon="el-icon-lock"
|
||||
@keyup.enter.native="handleRegister"
|
||||
>
|
||||
<i slot="suffix" class="el-icon-view password-eye" @click="regPwdVisible = !regPwdVisible"></i>
|
||||
<i slot="suffix" class="el-icon-view password-eye" @click="regPwdVisible = !regPwdVisible" />
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="agreement-wrap">
|
||||
<el-checkbox v-model="registerForm.agree">
|
||||
我已阅读并同意《用户协议》、《隐私政策》、《产品服务协议》
|
||||
我已阅读并同意
|
||||
<a class="agreement-link" :href="getAgreementUrl('user')" target="_blank" rel="noopener noreferrer" @click.stop>《用户协议》</a>
|
||||
、
|
||||
<a class="agreement-link" :href="getAgreementUrl('privacy')" target="_blank" rel="noopener noreferrer" @click.stop>《隐私政策》</a>
|
||||
|
||||
</el-checkbox>
|
||||
</div>
|
||||
|
||||
@ -234,6 +238,9 @@ export default {
|
||||
if (this.$refs.loginForm) this.$refs.loginForm.clearValidate()
|
||||
})
|
||||
},
|
||||
getAgreementUrl(type) {
|
||||
return `${window.location.origin}/#/agreement/${type}`
|
||||
},
|
||||
passwordEncryption(passwordUser) {
|
||||
const publicKey = '-----BEGIN PUBLIC KEY-----\n' +
|
||||
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApJ3ThUWT3CgvH0O8rrT6\n' +
|
||||
@ -381,24 +388,49 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
goAfterLogin(path) {
|
||||
if (this.$route.path === path) {
|
||||
this.$router.replace({
|
||||
path,
|
||||
query: {
|
||||
...this.$route.query,
|
||||
_route_refresh: Date.now()
|
||||
}
|
||||
}).catch(() => {})
|
||||
return
|
||||
}
|
||||
this.$router.push(path).catch(() => {})
|
||||
},
|
||||
redirectAfterLogin(res) {
|
||||
if (res.admin === 1) {
|
||||
this.goAfterLogin('/')
|
||||
return
|
||||
}
|
||||
if (String(res.org_type) === '2' || String(res.org_type) === '3') {
|
||||
sessionStorage.removeItem('loginRedirectPath')
|
||||
this.goAfterLogin(getHomePath())
|
||||
return
|
||||
}
|
||||
if (res.roles && res.roles.includes('客户')) {
|
||||
sessionStorage.removeItem('loginRedirectPath')
|
||||
this.goAfterLogin(getHomePath())
|
||||
return
|
||||
}
|
||||
if (res.roles && res.roles.includes('管理员')) {
|
||||
sessionStorage.removeItem('loginRedirectPath')
|
||||
this.$router.push('/superAdministrator/roleManagement').catch(() => {})
|
||||
this.goAfterLogin('/superAdministrator/roleManagement')
|
||||
return
|
||||
}
|
||||
const redirectPath = sessionStorage.getItem('loginRedirectPath')
|
||||
if (redirectPath && redirectPath.startsWith('/') && !redirectPath.includes('/login')) {
|
||||
sessionStorage.removeItem('loginRedirectPath')
|
||||
this.$router.push(redirectPath).catch(() => {})
|
||||
this.goAfterLogin(redirectPath)
|
||||
return
|
||||
}
|
||||
if (res.admin === 1) this.$router.push('/').catch(() => {})
|
||||
else if (String(res.org_type) === '2' || String(res.org_type) === '3') this.$router.push(getHomePath()).catch(() => {})
|
||||
else if (res.roles && res.roles.includes('运营')) this.$router.push('/operation/supplierManagement').catch(() => {})
|
||||
else if (res.roles && res.roles.includes('销售')) this.$router.push('/sales/distributorManagement').catch(() => {})
|
||||
else if (res.roles && res.roles.includes('运维')) this.$router.push('/operationAndMaintenance/workOrderProcessing').catch(() => {})
|
||||
else this.$router.push('/').catch(() => {})
|
||||
if (res.roles && res.roles.includes('运营')) this.goAfterLogin('/operation/supplierManagement')
|
||||
else if (res.roles && res.roles.includes('销售')) this.goAfterLogin('/sales/distributorManagement')
|
||||
else if (res.roles && res.roles.includes('运维')) this.goAfterLogin('/operationAndMaintenance/workOrderProcessing')
|
||||
else this.goAfterLogin('/')
|
||||
},
|
||||
handleRegister() {
|
||||
if (!this.registerForm.agree) {
|
||||
@ -760,6 +792,21 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
padding: 0;
|
||||
color: #1d4ed8;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.agreement-link:hover {
|
||||
color: #0f172a;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.bottom-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@ -811,4 +858,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<div class="contract-case-page" :class="{ 'has-fixed-tabs': isTabsFixed }">
|
||||
<div class="decor decor--blue"></div>
|
||||
<div class="decor decor--purple"></div>
|
||||
<div class="contract-case-page">
|
||||
<div class="decor decor--blue" />
|
||||
<div class="decor decor--purple" />
|
||||
|
||||
<section class="case-hero" :style="{ backgroundImage: `url(${bannerImg})` }">
|
||||
<div class="case-container">
|
||||
<div class="case-hero__content">
|
||||
<h1>合同智能审查</h1>
|
||||
<h1>{{ $t('casePages.contract.heroTitle') }}</h1>
|
||||
<p>
|
||||
面向企业法务与业务签约全流程的专业审查智能体,聚合法规条文、企业内部风控标准、历史合同案例多维信息,依托大模型深度语义解析能力自动识别合同法律隐患,智能生成标准化修改建议与风险说明。
|
||||
{{ $t('casePages.contract.heroDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section ref="caseTabs" class="case-tabs" :class="{ 'is-fixed': isTabsFixed }">
|
||||
<section class="case-tabs">
|
||||
<div class="case-container">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
v-for="tab in localizedTabs"
|
||||
:key="tab.id"
|
||||
type="button"
|
||||
class="tab-item"
|
||||
@ -33,8 +33,8 @@
|
||||
<section id="scenarios" class="case-section">
|
||||
<div class="case-container">
|
||||
<div class="section-heading">
|
||||
<h2>应用场景</h2>
|
||||
<p>覆盖企业合同全生命周期审核链路</p>
|
||||
<h2>{{ $t('casePages.contract.scenariosTitle') }}</h2>
|
||||
<p>{{ $t('casePages.contract.scenariosDesc') }}</p>
|
||||
</div>
|
||||
<div class="case-scenario-grid">
|
||||
<article class="case-feature-card scenario-card">
|
||||
@ -75,9 +75,9 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414A1 1 0 0119 9.414V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</span>
|
||||
<h3>业务合同初审</h3>
|
||||
<h3>{{ $t('casePages.contract.scenarios.0.title') }}</h3>
|
||||
</div>
|
||||
<p>自动解析购销、服务、租赁、合作类通用业务合同,逐条对标企业风控红线快速筛查风险点,输出初审意见,减轻法务基础审核工作量,快速完成业务前置审批。</p>
|
||||
<p>{{ $t('casePages.contract.scenarios.0.desc') }}</p>
|
||||
</article>
|
||||
|
||||
<article class="case-feature-card scenario-card">
|
||||
@ -109,9 +109,9 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</span>
|
||||
<h3>复杂商事合同深度风控</h3>
|
||||
<h3>{{ $t('casePages.contract.scenarios.1.title') }}</h3>
|
||||
</div>
|
||||
<p>针对投融资、知识产权、工程、保密竞业等高风险专项合同,联动完整法条与司法判例开展多层级风险推演,梳理权责漏洞、违约缺陷、管辖争议等深层隐患,输出完整风控评估文档。</p>
|
||||
<p>{{ $t('casePages.contract.scenarios.1.desc') }}</p>
|
||||
</article>
|
||||
|
||||
<article class="case-feature-card scenario-card">
|
||||
@ -144,9 +144,9 @@
|
||||
</g>
|
||||
<g class="diff-tags">
|
||||
<rect x="90" y="48" width="30" height="8" rx="2" fill="#ef4444" opacity="0.2" />
|
||||
<text x="95" y="54" font-size="8" fill="#ef4444" font-weight="bold">删除</text>
|
||||
<text x="95" y="54" font-size="8" fill="#ef4444" font-weight="bold">{{ $t('casePages.contract.deleted') || '删除' }}</text>
|
||||
<rect x="230" y="63" width="30" height="8" rx="2" fill="#10b981" opacity="0.2" />
|
||||
<text x="235" y="69" font-size="8" fill="#10b981" font-weight="bold">新增</text>
|
||||
<text x="235" y="69" font-size="8" fill="#10b981" font-weight="bold">{{ $t('casePages.contract.added') || '新增' }}</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
@ -156,9 +156,9 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
</svg>
|
||||
</span>
|
||||
<h3>多方合同版本比对修订</h3>
|
||||
<h3>{{ $t('casePages.contract.scenarios.2.title') }}</h3>
|
||||
</div>
|
||||
<p>自动识别甲乙双方多轮修改稿件差异,区分新增、删减、修改条款,高亮标注风险变更内容,同步生成版本对比台账,辅助商务谈判与法务复核,避免改稿遗漏关键风险。</p>
|
||||
<p>{{ $t('casePages.contract.scenarios.2.desc') }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
@ -167,8 +167,8 @@
|
||||
<section id="solution" class="case-section solution-section">
|
||||
<div class="case-container">
|
||||
<div class="section-heading">
|
||||
<h2>企业合同管理的困境与解决方案</h2>
|
||||
<p>围绕数据归集、合同编审、知识沉淀、智能咨询四大核心维度</p>
|
||||
<h2>{{ $t('casePages.contract.solutionTitle') }}</h2>
|
||||
<p>{{ $t('casePages.contract.solutionDesc') }}</p>
|
||||
</div>
|
||||
<div class="solution-grid">
|
||||
<div class="solution-column">
|
||||
@ -178,9 +178,9 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M4.93 19h14.14c1.54 0 2.5-1.67 1.73-3L13.73 4c-.77-1.33-2.69-1.33-3.46 0L3.2 16c-.77 1.33.19 3 1.73 3z" />
|
||||
</svg>
|
||||
</span>
|
||||
<h3>现存困境</h3>
|
||||
<h3>{{ $t('casePages.contract.challengeTitle') }}</h3>
|
||||
</div>
|
||||
<article v-for="item in problems" :key="item.title" class="solution-item solution-item--danger">
|
||||
<article v-for="item in localizedProblems" :key="item.title" class="solution-item solution-item--danger">
|
||||
<span class="solution-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="item.icon" />
|
||||
@ -200,9 +200,9 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
<h3>解决方案</h3>
|
||||
<h3>{{ $t('casePages.contract.abilityTitle') }}</h3>
|
||||
</div>
|
||||
<article v-for="item in solutions" :key="item.title" class="solution-item solution-item--primary">
|
||||
<article v-for="item in localizedSolutions" :key="item.title" class="solution-item solution-item--primary">
|
||||
<span class="solution-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="item.icon" />
|
||||
@ -221,11 +221,11 @@
|
||||
<section id="highlights" class="case-section">
|
||||
<div class="case-container">
|
||||
<div class="section-heading">
|
||||
<h2>项目亮点</h2>
|
||||
<p>全流程智能风控,四大核心审查能力落地</p>
|
||||
<h2>{{ $t('casePages.contract.highlightsTitle') }}</h2>
|
||||
<p>{{ $t('casePages.contract.highlightsDesc') }}</p>
|
||||
</div>
|
||||
<div class="case-highlight-grid">
|
||||
<article v-for="(item, index) in highlights" :key="item.title" class="case-highlight-card">
|
||||
<article v-for="(item, index) in localizedHighlights" :key="item.title" class="case-highlight-card">
|
||||
<span class="highlight-num">{{ index + 1 }}</span>
|
||||
<div>
|
||||
<h3>{{ item.title }}</h3>
|
||||
@ -237,7 +237,6 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -247,8 +246,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'scenarios',
|
||||
isTabsFixed: false,
|
||||
tabsOriginTop: 0,
|
||||
bannerImg: require('@/assets/image/bgimg.png'),
|
||||
tabs: [
|
||||
{ id: 'scenarios', label: '应用场景' },
|
||||
@ -334,11 +331,27 @@ export default {
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
localizedTabs() {
|
||||
return this.tabs.map(tab => ({
|
||||
...tab,
|
||||
label: this.$t(`casePages.contract.tabs.${tab.id}`)
|
||||
}))
|
||||
},
|
||||
localizedProblems() {
|
||||
return this.getLocalizedList(this.problems, 'casePages.contract.challenges')
|
||||
},
|
||||
localizedSolutions() {
|
||||
return this.getLocalizedList(this.solutions, 'casePages.contract.abilities')
|
||||
},
|
||||
localizedHighlights() {
|
||||
return this.getLocalizedList(this.highlights, 'casePages.contract.highlights')
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const scroller = this.getScrollContainer()
|
||||
this.resetScrollTop(scroller)
|
||||
this.refreshTabsOrigin()
|
||||
scroller.addEventListener('scroll', this.handlePageScroll, { passive: true })
|
||||
this.handlePageScroll()
|
||||
})
|
||||
@ -347,6 +360,13 @@ export default {
|
||||
this.getScrollContainer().removeEventListener('scroll', this.handlePageScroll)
|
||||
},
|
||||
methods: {
|
||||
getLocalizedList(list, keyPrefix) {
|
||||
return list.map((item, index) => ({
|
||||
...item,
|
||||
title: this.$t(`${keyPrefix}.${index}.title`),
|
||||
desc: this.$t(`${keyPrefix}.${index}.desc`)
|
||||
}))
|
||||
},
|
||||
getScrollContainer() {
|
||||
return document.getElementById('homeOut') || window
|
||||
},
|
||||
@ -356,15 +376,6 @@ export default {
|
||||
} else {
|
||||
scroller.scrollTop = 0
|
||||
}
|
||||
this.isTabsFixed = false
|
||||
},
|
||||
refreshTabsOrigin() {
|
||||
const scroller = this.getScrollContainer()
|
||||
this.tabsOriginTop = this.getElementTopInScroller(this.$refs.caseTabs, scroller)
|
||||
},
|
||||
getScrollOffset() {
|
||||
const tabs = this.$el.querySelector('.case-tabs')
|
||||
return (tabs ? tabs.offsetHeight : 0) + 12
|
||||
},
|
||||
getElementTopInScroller(element, scroller) {
|
||||
if (scroller === window) {
|
||||
@ -380,14 +391,13 @@ export default {
|
||||
const element = this.$el.querySelector(`#${sectionName}`)
|
||||
if (!element) return
|
||||
const scroller = this.getScrollContainer()
|
||||
const top = this.getElementTopInScroller(element, scroller) - this.getScrollOffset()
|
||||
const top = this.getElementTopInScroller(element, scroller)
|
||||
scroller.scrollTo({ top, behavior: 'smooth' })
|
||||
},
|
||||
handlePageScroll() {
|
||||
const scroller = this.getScrollContainer()
|
||||
const scrollerTop = this.getScrollerTop(scroller)
|
||||
this.isTabsFixed = scrollerTop >= this.tabsOriginTop
|
||||
const offsetTop = scrollerTop + this.getScrollOffset() + 24
|
||||
const offsetTop = scrollerTop + 24
|
||||
const currentTab = this.tabs
|
||||
.map(tab => {
|
||||
const element = this.$el.querySelector(`#${tab.id}`)
|
||||
@ -493,27 +503,12 @@ export default {
|
||||
}
|
||||
|
||||
.case-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 80;
|
||||
height: 60px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.case-tabs.is-fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.contract-case-page.has-fixed-tabs .case-main {
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
.case-tabs .case-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
<template>
|
||||
<div class="decision-case-page" :class="{ 'has-fixed-tabs': isTabsFixed }">
|
||||
<div class="decor decor--blue"></div>
|
||||
<div class="decor decor--purple"></div>
|
||||
<div class="decor decor--deep-blue"></div>
|
||||
<div class="decor decor--deep-purple"></div>
|
||||
<div class="decision-case-page">
|
||||
<div class="decor decor--blue" />
|
||||
<div class="decor decor--purple" />
|
||||
<div class="decor decor--deep-blue" />
|
||||
<div class="decor decor--deep-purple" />
|
||||
|
||||
<section class="case-hero">
|
||||
<img class="case-hero__bg" :src="bannerImg" alt="" @load="refreshTabsOrigin">
|
||||
<img class="case-hero__bg" :src="bannerImg" alt="">
|
||||
<div class="case-container">
|
||||
<div class="case-hero__content">
|
||||
<h1>投策智能体</h1>
|
||||
<h1>{{ $t('casePages.decision.heroTitle') }}</h1>
|
||||
<p>
|
||||
面向企业投资决策的辅助智能体,整合多维度市场数据、行业趋势与政策信息,通过算法模拟不同决策场景的收益与风险,自动生成可视化分析报告。帮助企业快速梳理核心信息,预判市场走向,辅助降低决策偏差,提升投资方案的科学性与可行性。
|
||||
{{ $t('casePages.decision.heroDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section ref="caseTabs" class="case-tabs" :class="{ 'is-fixed': isTabsFixed }">
|
||||
<section class="case-tabs">
|
||||
<div class="case-container">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
v-for="tab in localizedTabs"
|
||||
:key="tab.id"
|
||||
type="button"
|
||||
class="tab-item"
|
||||
@ -36,11 +36,11 @@
|
||||
<section id="scenarios" class="case-section">
|
||||
<div class="case-container">
|
||||
<div class="section-heading">
|
||||
<h2>应用场景</h2>
|
||||
<p>覆盖企业投资决策全链路</p>
|
||||
<h2>{{ $t('casePages.decision.scenariosTitle') }}</h2>
|
||||
<p>{{ $t('casePages.decision.scenariosDesc') }}</p>
|
||||
</div>
|
||||
<div class="scenario-grid">
|
||||
<article v-for="item in scenarios" :key="item.title" class="feature-card">
|
||||
<article v-for="item in localizedScenarios" :key="item.title" class="feature-card">
|
||||
<div class="feature-card__head">
|
||||
<div class="icon-gradient">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@ -58,8 +58,8 @@
|
||||
<section id="solution" class="case-section">
|
||||
<div class="case-container">
|
||||
<div class="section-heading">
|
||||
<h2>企业决策的困境与解决方案</h2>
|
||||
<p>聚焦数据收集、研报撰写、知识管理、智能应用四大维度</p>
|
||||
<h2>{{ $t('casePages.decision.solutionTitle') }}</h2>
|
||||
<p>{{ $t('casePages.decision.solutionDesc') }}</p>
|
||||
</div>
|
||||
<div class="solution-grid">
|
||||
<article class="problem-card">
|
||||
@ -67,10 +67,10 @@
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
困境:数据丰富,洞察贫乏
|
||||
{{ $t('casePages.decision.challengeTitle') }}
|
||||
</h3>
|
||||
<div class="list-block">
|
||||
<div v-for="item in problems" :key="item.title" class="list-item list-item--danger">
|
||||
<div v-for="item in localizedProblems" :key="item.title" class="list-item list-item--danger">
|
||||
<div class="list-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="item.icon" />
|
||||
@ -89,13 +89,13 @@
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
解决方案:E决策平台核心能力
|
||||
{{ $t('casePages.decision.abilityTitle') }}
|
||||
</h3>
|
||||
<p class="solution-card__intro">
|
||||
聚焦“数据收集、研报撰写、知识管理、智能应用”四大维度,帮助企业构建专属知识体系,赋能科学决策。
|
||||
{{ $t('casePages.decision.abilityDesc') }}
|
||||
</p>
|
||||
<div class="ability-list">
|
||||
<div v-for="item in abilities" :key="item.title" class="ability-item">
|
||||
<div v-for="item in localizedAbilities" :key="item.title" class="ability-item">
|
||||
<div class="ability-icon icon-gradient">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="item.icon" />
|
||||
@ -115,11 +115,11 @@
|
||||
<section id="highlights" class="case-section">
|
||||
<div class="case-container">
|
||||
<div class="section-heading">
|
||||
<h2>项目亮点</h2>
|
||||
<p>一次研究,双格式交付</p>
|
||||
<h2>{{ $t('casePages.decision.highlightsTitle') }}</h2>
|
||||
<p>{{ $t('casePages.decision.highlightsDesc') }}</p>
|
||||
</div>
|
||||
<div class="highlight-grid">
|
||||
<article v-for="item in highlights" :key="item.title" class="feature-card">
|
||||
<article v-for="item in localizedHighlights" :key="item.title" class="feature-card">
|
||||
<div class="feature-card__head">
|
||||
<div class="icon-gradient">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@ -137,13 +137,13 @@
|
||||
|
||||
<section class="case-cta">
|
||||
<div class="case-container case-cta__inner">
|
||||
<h2>开始使用投策智能体</h2>
|
||||
<p>让AI赋能您的投资决策,一次研究,双格式交付</p>
|
||||
<button type="button" @click="handleContact">联系销售</button>
|
||||
<h2>{{ $t('casePages.decision.ctaTitle') }}</h2>
|
||||
<p>{{ $t('casePages.decision.ctaDesc') }}</p>
|
||||
<button type="button" @click="handleContact">{{ $t('casePages.decision.contactSales') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Talk></Talk>
|
||||
<Talk />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -156,8 +156,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'scenarios',
|
||||
isTabsFixed: false,
|
||||
tabsOriginTop: 0,
|
||||
tabs: [
|
||||
{ id: 'scenarios', label: '应用场景' },
|
||||
{ id: 'solution', label: '解决方案' },
|
||||
@ -234,11 +232,30 @@ export default {
|
||||
bannerImg: require('./img.jpg')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
localizedTabs() {
|
||||
return this.tabs.map(tab => ({
|
||||
...tab,
|
||||
label: this.$t(`casePages.decision.tabs.${tab.id}`)
|
||||
}))
|
||||
},
|
||||
localizedScenarios() {
|
||||
return this.getLocalizedList(this.scenarios, 'casePages.decision.scenarios')
|
||||
},
|
||||
localizedProblems() {
|
||||
return this.getLocalizedList(this.problems, 'casePages.decision.challenges')
|
||||
},
|
||||
localizedAbilities() {
|
||||
return this.getLocalizedList(this.abilities, 'casePages.decision.abilities')
|
||||
},
|
||||
localizedHighlights() {
|
||||
return this.getLocalizedList(this.highlights, 'casePages.decision.highlights')
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const scroller = this.getScrollContainer()
|
||||
this.resetScrollTop(scroller)
|
||||
this.refreshTabsOrigin()
|
||||
scroller.addEventListener('scroll', this.handlePageScroll, { passive: true })
|
||||
this.handlePageScroll()
|
||||
})
|
||||
@ -247,6 +264,13 @@ export default {
|
||||
this.getScrollContainer().removeEventListener('scroll', this.handlePageScroll)
|
||||
},
|
||||
methods: {
|
||||
getLocalizedList(list, keyPrefix) {
|
||||
return list.map((item, index) => ({
|
||||
...item,
|
||||
title: this.$t(`${keyPrefix}.${index}.title`),
|
||||
desc: this.$t(`${keyPrefix}.${index}.desc`)
|
||||
}))
|
||||
},
|
||||
getScrollContainer() {
|
||||
return document.getElementById('homeOut') || window
|
||||
},
|
||||
@ -256,16 +280,6 @@ export default {
|
||||
} else {
|
||||
scroller.scrollTop = 0
|
||||
}
|
||||
this.isTabsFixed = false
|
||||
},
|
||||
refreshTabsOrigin() {
|
||||
const scroller = this.getScrollContainer()
|
||||
this.tabsOriginTop = this.getElementTopInScroller(this.$refs.caseTabs, scroller)
|
||||
},
|
||||
getScrollOffset() {
|
||||
const tabs = this.$el.querySelector('.case-tabs')
|
||||
const tabsHeight = tabs ? tabs.offsetHeight : 0
|
||||
return tabsHeight + 12
|
||||
},
|
||||
getElementTopInScroller(element, scroller) {
|
||||
if (scroller === window) {
|
||||
@ -281,16 +295,14 @@ export default {
|
||||
const element = this.$el.querySelector(`#${sectionName}`)
|
||||
if (element) {
|
||||
const scroller = this.getScrollContainer()
|
||||
const offset = this.getScrollOffset()
|
||||
const top = this.getElementTopInScroller(element, scroller) - offset
|
||||
const top = this.getElementTopInScroller(element, scroller)
|
||||
scroller.scrollTo({ top, behavior: 'smooth' })
|
||||
}
|
||||
},
|
||||
handlePageScroll() {
|
||||
const scroller = this.getScrollContainer()
|
||||
const scrollerTop = this.getScrollerTop(scroller)
|
||||
this.isTabsFixed = scrollerTop >= this.tabsOriginTop
|
||||
const offsetTop = scrollerTop + this.getScrollOffset() + 24
|
||||
const offsetTop = scrollerTop + 24
|
||||
const currentTab = this.tabs
|
||||
.map(tab => {
|
||||
const element = this.$el.querySelector(`#${tab.id}`)
|
||||
@ -441,27 +453,12 @@ export default {
|
||||
}
|
||||
|
||||
.case-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 80;
|
||||
background: #fff;
|
||||
height: 60px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.case-tabs.is-fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.decision-case-page.has-fixed-tabs .case-main {
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
.case-tabs .case-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
103
f/web-kboss/src/views/homePage/agreement/AgreementPreview.vue
Normal file
103
f/web-kboss/src/views/homePage/agreement/AgreementPreview.vue
Normal file
@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<main class="agreement-preview-page">
|
||||
<section class="agreement-preview-card">
|
||||
<article class="agreement-content">
|
||||
<p
|
||||
v-for="(paragraph, index) in currentDoc.paragraphs"
|
||||
:key="`${paragraph.slice(0, 12)}-${index}`"
|
||||
class="agreement-paragraph"
|
||||
:class="{
|
||||
'agreement-paragraph--heading': isHeadingParagraph(paragraph),
|
||||
'agreement-paragraph--sub': isSubParagraph(paragraph)
|
||||
}"
|
||||
v-html="paragraph"
|
||||
/>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import docs from './agreementDocs'
|
||||
|
||||
export default {
|
||||
name: 'AgreementPreview',
|
||||
computed: {
|
||||
currentDoc() {
|
||||
return docs[this.$route.params.type] || docs.user
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isHeadingParagraph(paragraph) {
|
||||
return /^<strong>[^<]+<\/strong>$/.test(String(paragraph || '').trim())
|
||||
},
|
||||
isSubParagraph(paragraph) {
|
||||
const text = String(paragraph || '').trim()
|
||||
return /^(?:\d+\.\d+\.|\d+\.\s|[0-9]+\.\s)/.test(text.replace(/<[^>]+>/g, ''))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.agreement-preview-page {
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
padding: 48px 24px 64px;
|
||||
overflow-y: auto;
|
||||
color: #111827;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.agreement-preview-card {
|
||||
width: min(920px, 100%);
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.agreement-content {
|
||||
color: #374151;
|
||||
font-size: 15px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
.agreement-content p {
|
||||
margin: 0 0 14px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.agreement-content p.agreement-paragraph--heading {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 10px;
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.agreement-content p.agreement-paragraph--sub {
|
||||
padding-left: 1.25em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
::v-deep .agreement-content strong {
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.agreement-content p:first-child {
|
||||
color: #0f172a;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.agreement-content p:first-child ::v-deep strong {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.agreement-preview-page {
|
||||
padding: 28px 16px 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
147
f/web-kboss/src/views/homePage/agreement/agreementDocs.js
Normal file
147
f/web-kboss/src/views/homePage/agreement/agreementDocs.js
Normal file
@ -0,0 +1,147 @@
|
||||
export default {
|
||||
'user': {
|
||||
'title': '用户协议',
|
||||
'paragraphs': [
|
||||
'<strong>开元云用户协议</strong>',
|
||||
'更新日期:【2026】年【7】月【30】日',
|
||||
'生效日期:【2026】年【7】月【30】日',
|
||||
'本《开元云用户协议》(以下简称“本协议”)由开元云(北京)科技有限公司(以下简称“开元云”或“我们”)与注册、访问或使用开元云服务的自然人、法人或非法人组织(以下简称“用户”或“您”)订立。',
|
||||
'<strong>请您在注册、下单或使用服务前完整阅读本协议。与责任限制、费用、服务暂停或终止、数据处理及争议解决有关的条款已采用加粗方式提示。您点击同意、提交订单、开通或实际使用服务,均表示您已阅读并同意本协议。您代表法人或其他组织操作的,应保证已取得充分授权。</strong>',
|
||||
'<strong>1. 协议范围与定义</strong>',
|
||||
'1.1. 开元云平台,是指由开元云运营的网站、业务平台、客户端、控制台、应用程序、接口及其他服务载体,包括开元算力应用服务平台及后续更名、升级或新增的服务载体。',
|
||||
'1.2. 开元云服务,是指开元云通过平台或双方确认的订单提供的云计算、算力、算力网络、人工智能及相关产品和技术服务,具体以产品页面、订单或服务说明为准。',
|
||||
'1.3. 开元云既可能直接提供服务,也可能代理销售、转售或协助交付第三方服务。实际销售方、服务提供方、开票方及售后责任方,以订单记载为准。',
|
||||
'1.4. 订单,是指用户通过平台提交并由系统确认的订单,以及双方签署或确认的报价单、采购单、服务单、工作说明书或其他交易文件。订单应列明或能够确定服务内容、服务期限、计费方式、实际销售方或服务提供方、开票方、售后责任方及适用的服务规则。',
|
||||
'1.5. 本协议与订单、产品说明、服务等级协议、隐私政策及适用的平台规则共同构成双方协议。约定不一致时,按双方专项书面约定、订单、产品专项规则、本协议的顺序适用;法律另有规定的除外。',
|
||||
'<strong>2. 账户注册与管理</strong>',
|
||||
'2.1. 您应按要求提供真实、准确、完整、有效的注册及认证信息,并在信息变化后及时更新。特定服务依法需要实名核验、资质审查或安全评估的,您应予配合。',
|
||||
'2.2. 您应妥善保管账户及认证信息,并合理设置操作权限。通过您账户实施的操作原则上视为您的行为,但您能够证明账户被未经授权使用且已及时采取合理措施的除外。',
|
||||
'2.3. 发现账户或认证信息被盗用、泄露或存在异常操作时,您应及时通知开元云并采取必要措施。开元云可根据风险采取身份复核、限制登录或临时冻结等措施。',
|
||||
'2.4. 未经开元云同意,您不得转让、出租、出借账户,或者以共享账户等方式规避计费、资质审查或访问控制。',
|
||||
'<strong>3. 服务订购与交付</strong>',
|
||||
'3.1. 服务内容、期限、费用、交付方式、技术指标及支持标准,以产品页面、订单或双方书面约定为准。您应在下单前核对相关信息。',
|
||||
'3.2. 平台展示内容符合要约条件且订单提交成功的,订单成立;订单明确需经资源、资质或合规审核的,以开元云确认、服务开通或双方签署书面文件时成立。',
|
||||
'3.3. 对开元云直接提供的服务,开元云按约承担交付、维护及售后责任。对第三方服务,各方责任依订单及订购前向您提供的第三方规则确定。<strong>开元云的代理或平台角色不免除其依法应承担的信息披露、平台管理及因自身过错产生的责任。</strong>',
|
||||
'3.4. 服务可通过账户开通、资源交付、接口启用、部署完成或订单约定的方式交付。需要验收的,按订单约定办理;订单未约定验收期限的,您应在收到交付通知后五个工作日内完成合理核验并反馈问题,隐蔽缺陷不受该期限限制。',
|
||||
'3.5. 服务变更、扩容、迁移或定制开发,应通过订单、变更单或双方书面确认实施。',
|
||||
'<strong>4. 订购、交付与验收</strong>',
|
||||
'4.1. 您应在下单前核对服务名称、配置、数量、价格、第三方服务商及限制等条件。平台提供订单更正和取消入口的,您可在订单确认前修改;订单确认后的变更、退订或迁移按对应产品规则或双方约定办理。',
|
||||
'4.2. 平台展示的信息符合要约条件且用户提交订单成功的,订单成立;平台明确为要约邀请,或订单需经资源、资质、信用或合规审核的,以开元云确认、资源开通或双方签署书面文件时成立。开元云不得以格式条款约定用户付款后合同仍不成立。',
|
||||
'4.3. 服务通过账户开通、资源交付、许可码发送、接口启用、部署完成或订单约定的其他方式交付。需要验收的,验收标准和期限以订单为准;订单未约定的,用户应在收到交付通知后五个工作日内完成合理核验并反馈可复现的问题,不影响用户依法就隐蔽缺陷主张权利。',
|
||||
'4.4. 按周期续费的服务,以订单是否设置自动续费为准。开元云在自动扣款前应以合理方式提示扣款金额和时间,并提供便捷的取消方式;用户取消后不再产生下一周期费用,但已生效周期按约定继续履行。',
|
||||
'4.5. 资源扩容、迁移、版本升级、服务内容变更或定制开发,应通过新订单、变更单或双方书面确认实施。未经确认的口头沟通不构成对服务范围、费用或期限的变更。',
|
||||
'<strong>5. 费用、支付、发票与退款</strong>',
|
||||
'<strong>5.1. 您应按照订单约定的价格、计费单位、结算周期和支付期限付款。因用户使用量、配置或调用次数变化产生的费用,以平台计量记录和订单计费规则为准;用户对计量结果有异议的,应在账单出具后十五日内提出,开元云应提供合理的核验渠道。</strong>',
|
||||
'5.2. 除订单另有约定,价格不含因用户自身支付方式产生的银行手续费或其他第三方费用。开元云调整服务价格的,应在调整生效前通过平台公告、站内信、电子邮件或订单页面通知;已生效的固定期限订单不受影响,按量服务和续费周期自通知载明的日期适用新价格。',
|
||||
'5.3. 发票由订单或结算页面列明的开票主体依法开具。您应提供真实、准确的开票信息;因信息错误造成的重开、红冲或邮寄成本,由责任方承担。',
|
||||
'<strong>5.4. 退订、退款及未消费余额的处理,以订单和对应产品规则为准。涉及第三方服务的,还受第三方服务商的退订条件和资源采购规则约束。</strong>',
|
||||
'5.5. 用户逾期付款的,开元云可催告并给予合理补救期限;用户在期限届满后仍未付款的,开元云可暂停相应服务并按订单约定收取违约金。暂停前,开元云应在合理可行范围内提示用户备份或迁移数据;紧急安全风险、恶意欠费或法律要求立即暂停的除外。',
|
||||
'<strong>6. 用户数据与个人信息</strong>',
|
||||
'6.1. 用户对其通过服务上传、生成、存储或处理的数据和内容(以下简称“用户数据”)依法享有相应权利。除履行协议、维护安全、遵守法律或取得用户授权外,开元云不取得用户数据的所有权,也不将其用于与提供服务无关的目的。',
|
||||
'6.2. 开元云为账户管理、交易结算、客户服务、安全保障及依法运营而处理个人信息的,按照《开元云隐私政策》执行。',
|
||||
'6.3. 用户决定个人信息处理目的和方式、开元云仅按用户指示提供处理能力的,用户应保证具有合法处理依据;开元云应按照约定和用户的合法指示处理,并采取与风险相适应的安全措施。必要时,双方可另行签署数据处理协议。',
|
||||
'<strong>6.4. 为履行订单确需委托第三方处理或向第三方提供用户数据的,开元云应依法履行告知、合同约束及安全管理义务。未经合法依据,不得向无关第三方提供用户个人信息。</strong>',
|
||||
'6.5. 涉及数据跨境的,由依法负有责任的一方完成相应合规程序。开元云不得在未告知用户的情况下擅自改变订单约定的数据存储地域。',
|
||||
'6.6. 开元云应采取必要的网络和数据安全措施。发生可能影响用户权益的安全事件时,应及时采取补救措施,并按法律规定和合同约定履行通知、报告义务。',
|
||||
'6.7. 服务终止后,开元云按照订单、产品规则或数据处理协议提供合理的数据导出期限,并在期限届满后依法删除或匿名化处理用户数据;依法需要留存的除外。',
|
||||
'<strong>7. 知识产权</strong>',
|
||||
'7.1. 开元云平台、软件、接口、文档、商标及相关技术成果的知识产权归开元云或相应权利人所有。未经许可,用户不得超出订单及产品规则约定的范围使用。',
|
||||
'7.2. 服务期限内,用户获得仅限自身合法业务使用的、非独占且不可转让的使用权。第三方产品或开源组件适用其各自许可条款。',
|
||||
'7.3. 用户数据及用户自行开发成果的权利,依法或依双方约定确定。用户授权开元云在提供和保障服务所必需的范围内处理用户数据。<strong>未经用户另行明确授权,开元云不得使用用户的非公开业务数据训练面向不特定用户的通用模型。</strong>',
|
||||
'7.4. 人工智能或模型服务的输出可能存在不准确或权利瑕疵。用户应结合使用场景进行核验,并依法处理输出内容的使用及权利风险。',
|
||||
'<strong>8. 服务运行与变更</strong>',
|
||||
'8.1. 开元云按照订单或服务等级协议维护其直接提供的服务。第三方服务的可用性、维护及补偿标准,以订单和第三方规则为准;开元云作出更高承诺的,从其承诺。',
|
||||
'8.2. 因维护升级需要计划中断服务的,开元云应按约提前通知;遇有安全事件、重大故障或主管机关要求等紧急情况,可先行处置并及时通知。',
|
||||
'8.3. 开元云可以对服务进行合理升级或调整。涉及主要功能、关键技术指标、数据处理方式或费用的重大变化,应提前合理通知;变化实质影响合同目的的,用户可依法解除受影响的服务。',
|
||||
'8.4. 第三方停止或变更服务导致迁移、替换或退订的,开元云应及时通知并提供合理处理方案。费用和责任按照订单、第三方规则及各方过错确定。',
|
||||
'<strong>9. 服务暂停与终止</strong>',
|
||||
'9.1. 用户可按订单和产品规则申请退订或停止续费。固定期限服务到期且未续费的,服务终止。',
|
||||
'<strong>9.2. 用户逾期付款、严重违反本协议、造成现实安全风险,或者依法需要暂停服务的,开元云可根据风险限制或暂停相关服务。除紧急情形外,开元云应事先通知并给予合理补救期限。</strong>',
|
||||
'9.3. 一方严重违约且在合理期限内未改正的,守约方可解除受影响的订单;违约无法补救或导致重大安全风险的,可立即解除。',
|
||||
'9.4. 服务终止不影响终止前已经产生的付款、保密、数据处理、责任承担及争议解决义务。用户数据按照第6.7条处理。',
|
||||
'<strong>10. 保证、免责与责任限制</strong>',
|
||||
'10.1. 开元云应以符合行业合理标准的技术和管理措施提供服务。除订单、产品说明或法律另有规定外,开元云不保证服务适合用户的特定目的或实现特定结果。',
|
||||
'<strong>10.2. 人工智能、模型、智能分析及计算结果可能存在错误、遗漏或偏差,仅供辅助使用,不能替代专业审查、验证或依法应由人工作出的决定。本条不免除开元云因虚假宣传、违反明示承诺或自身过错依法应承担的责任。</strong>',
|
||||
'10.3. 因用户自身原因或不属于开元云控制范围的第三方原因造成的损失,由责任方依法承担;开元云对其自身过错承担相应责任。',
|
||||
'<strong>10.4. 在法律允许的范围内,一方仅对其违约或过错造成的直接且可合理预见的损失承担责任。除法律规定不得限制的责任以及开元云故意、重大过失外,开元云就单一订单承担的累计赔偿责任,以索赔事件发生前十二个月内用户就该订单项下受影响服务实际支付的费用总额为上限;订单另有约定的,从其约定。</strong>',
|
||||
'10.5. 本协议的免责或限责条款不适用于法律禁止免责或限制责任的情形,也不影响消费者依法享有的权利。',
|
||||
'<strong>11. 保密</strong>',
|
||||
'11.1. 一方因订立或履行本协议知悉的对方非公开技术、经营、数据及其他依其性质应属保密的信息,均为保密信息。接收方仅可为履行本协议使用,并应采取合理保护措施。',
|
||||
'11.2. 已经合法公开、从无保密义务的第三方合法取得、接收方能够证明独立取得,或披露方同意公开的信息,不受前款限制。依法应披露的,接收方可在法定范围内披露,并在法律允许时通知披露方。',
|
||||
'11.3. 保密义务在协议终止后持续五年;商业秘密、个人信息及重要数据依法律规定持续保护。',
|
||||
'<strong>12. 不可抗力与通知</strong>',
|
||||
'12.1. 因不能预见、不能避免且不能克服的事件导致不能履行的,受影响方在法律允许范围内部分或全部免责,但迟延履行后发生不可抗力的除外。受影响方应及时通知并采取合理减损措施。',
|
||||
'12.2. 不可抗力持续超过六十日且导致合同目的不能实现的,任一方可解除受影响的未履行部分,已实际履行的服务按履行情况结算。',
|
||||
'12.3. 开元云可通过平台公告、站内信、账户通知、电子邮件、短信或订单约定的方式发送通知。涉及费用、主要功能、数据处理、服务暂停终止或争议解决的重大事项,应以能够合理到达用户的显著方式通知。',
|
||||
'12.4. 您应及时维护有效联系方式。因您未及时更新导致通知无法送达的,由您承担相应后果;开元云明知联系方式失效仍向该地址发送的除外。',
|
||||
'<strong>12.5. 开元云可因法律变化、服务调整或安全需要修改本协议。对用户权利义务有重大影响的修改,应在生效前显著通知。用户不同意的,可在生效前停止使用并按适用规则终止未履行服务。法律另有规定的,从其规定。</strong>',
|
||||
'<strong>13. 法律适用与其他</strong>',
|
||||
'13.1. 本协议的订立、效力、解释、履行及争议解决适用中华人民共和国大陆地区法律。',
|
||||
'13.2. 双方应先协商解决争议。协商不成的,任一方可向开元云住所地有管辖权的人民法院提起诉讼;消费者依法有权选择其他有管辖权法院的,从其规定。',
|
||||
'13.3. 双方是独立合同主体。本协议不成立合伙、合资、劳动或未经明确授权的代理关系。',
|
||||
'13.4. 一方未行使或迟延行使权利,不构成放弃。部分条款无效或不可执行的,不影响其他条款的效力。',
|
||||
'13.5. 本协议以电子形式订立,与纸质协议具有同等法律效力。用户可通过平台或客服获取、保存和下载协议文本及交易记录。',
|
||||
'13.6. 客服联系方式:【400-6150805 010-65917875】;电子邮箱:【Open-computing@kaiyuancloud.cn】。联系方式变更的,以开元云平台依法公示的信息为准。'
|
||||
]
|
||||
},
|
||||
'privacy': {
|
||||
'title': '隐私政策',
|
||||
'paragraphs': [
|
||||
'<strong>开元云隐私政策</strong>',
|
||||
'版本更新日期:【2026年7月30日】',
|
||||
'版本生效日期:【2026年7月30日】',
|
||||
'开元云(北京)科技有限公司(以下简称“开元云”或“我们”)重视您的个人信息和隐私保护。本政策适用于我们通过开元云官方网站、开放算力应用服务平台及其他由我们运营并明确适用本政策的产品和服务(统称“开元云服务”)处理个人信息的活动。某项服务另有专门隐私规则的,专门规则优先适用;未约定的事项,适用本政策。',
|
||||
'开元云服务涉及算力服务、算力网络、AI应用、云平台和相关技术服务。您代表单位使用服务的,请确认已取得必要授权;您向我们提供他人个人信息的,应当确保来源合法,并已依法履行告知、取得同意等义务。',
|
||||
'请您在使用开元云服务前阅读本政策。涉及敏感个人信息、向其他个人信息处理者提供个人信息或向境外提供个人信息等依法需要单独同意的事项,我们将另行告知并依法取得您的单独同意。',
|
||||
'<strong>一、定义</strong>',
|
||||
'1. <strong>个人信息:</strong>以电子或者其他方式记录的、与已识别或者可识别的自然人有关的各种信息,不包括匿名化处理后的信息。',
|
||||
'2. <strong>敏感个人信息:</strong>一旦泄露或者被非法使用,容易导致自然人的人格尊严受到侵害或者人身、财产安全受到危害的个人信息。开元云仅在特定目的、充分必要并采取严格保护措施的情况下处理敏感个人信息。',
|
||||
'3. <strong>匿名化:</strong>个人信息经过处理无法识别特定自然人且不能复原的过程。',
|
||||
'4. <strong>用户业务数据:</strong>用户在使用开元云服务过程中上传、生成、存储、传输或委托开元云处理的数据,不当然属于个人信息;其中含有个人信息的部分,依照适用法律和双方约定处理。',
|
||||
'<strong>二、我们如何收集和使用个人信息</strong>',
|
||||
'我们遵循合法、正当、必要和诚信原则,仅为明确、合理且与服务直接相关的目的处理个人信息。具体功能所需信息以实际页面、订单、合同或单独告知为准,主要包括:',
|
||||
'1. <strong>账号和联系信息。</strong>当您注册、登录或管理账号时,我们可能处理您的账号名称、手机号码、电子邮箱、密码密文及账号安全设置,用于创建账号、身份验证、发送必要通知和保障账号安全。拒绝提供必要信息可能导致无法使用账号功能,但通常不影响浏览公开内容。',
|
||||
'2. <strong>企业认证和交易信息。</strong>当您申请试用、购买算力或其他服务、签订或履行合同、结算或开具发票时,我们可能处理单位名称、统一社会信用代码、联系人及联系方式、服务配置、订单、合同、支付状态和开票信息,用于确认交易主体、交付服务、结算、售后和财务管理。支付机构直接处理的银行卡等信息由其依照自身规则处理,我们原则上仅接收完成交易所需的支付结果。',
|
||||
'3. <strong>咨询和服务记录。</strong>当您提交咨询、工单、投诉或参加业务活动时,我们可能处理您提交的姓名、联系方式、单位、需求描述、沟通记录和附件,用于回复请求、排查问题和改进服务。非提供服务所必需的调研或推广信息,我们将在必要时另行取得同意,并提供便捷的退订方式。',
|
||||
'4. <strong>设备、日志和安全信息。</strong>您访问或使用开元云服务时,我们可能自动记录IP地址、浏览器和设备类型、访问时间、操作记录、故障日志及安全日志,用于运行服务、定位故障、统计基本使用情况和防范网络攻击、欺诈或其他安全风险。我们不会仅因设备或日志信息而不合理限制您的服务。',
|
||||
'5. <strong>依法需要核验的信息。</strong>如特定产品、交易或监管要求确需进行个人或企业身份核验,我们可能处理姓名、身份证明或主体资质等必要信息,并在收集前另行说明具体目的、方式、范围和保存期限。涉及敏感个人信息的,我们将采取更严格的保护措施并依法取得单独同意;如有非敏感的替代核验方式,我们将按实际情况提供。',
|
||||
'我们依据您的同意、订立或履行合同所必需、履行法定义务,或者法律规定的其他基础处理个人信息。处理目的、方式或个人信息种类发生变化的,我们将依法重新告知,并在需要时重新取得同意。除法律另有规定外,您可以撤回基于同意作出的授权;撤回不影响撤回前处理活动的效力。',
|
||||
'<strong>三、Cookie和同类技术</strong>',
|
||||
'1. 为保障网站正常运行、保持登录状态、保存必要设置、分析故障和防范安全风险,我们可能使用Cookie和同类技术。必要Cookie被禁用后,部分功能可能无法正常使用;对于非必要Cookie,我们将依法提供选择或关闭方式。',
|
||||
'2. 您可以通过浏览器设置管理或删除Cookie。清除或拒绝Cookie可能使您需要重新登录或设置偏好,但不影响与相关Cookie无关的服务。我们不会将Cookie用于本政策未说明的目的。',
|
||||
'<strong>四、用户业务数据</strong>',
|
||||
'1. 您或您的单位在使用算力、存储、网络、模型、智能文档处理或其他开元云服务时上传、生成、存储、传输或委托我们处理的数据,属于用户业务数据。其中含有个人信息的,您或您的单位通常决定处理目的和方式,我们按照双方协议和您的合法指示提供受托处理服务。',
|
||||
'2. 您应当确保业务数据来源、内容及处理活动合法,并根据适用法律向相关个人履行告知、取得同意等义务。除非产品说明、订单或合同另有明确约定并具备合法处理基础,或者法律另有规定,我们不会将用户业务数据用于自身营销、训练通用模型或其他与提供约定服务无关的目的。',
|
||||
'用户业务数据与我们为账号管理、交易结算、客户支持和安全保障而独立处理的信息,适用不同的责任分工。具体数据位置、备份、迁移、删除和安全要求,以对应产品说明、订单或合同为准。',
|
||||
'<strong>五、委托处理、对外提供、转让和公开披露</strong>',
|
||||
'1. 为提供和保障开元云服务,我们可能委托基础设施、算力、网络、模型接口、身份核验、支付结算、电子签约、发票、客服或安全服务提供方处理必要的个人信息。我们将根据服务性质选择合作方,通过合同约定处理目的、期限、方式、信息种类、安全措施和双方责任,并进行必要监督。合作方不得将受托信息用于自身目的。',
|
||||
'2. 如需向其他个人信息处理者提供您的个人信息,我们将依法告知接收方信息、处理目的、方式和个人信息种类,并在法律要求时取得您的单独同意。涉及敏感个人信息的,我们还会说明必要性及对个人权益的影响。我们不会出售个人信息。',
|
||||
'3. 发生合并、分立、重组、资产转让或类似交易而需要转移个人信息的,我们将向您告知接收方信息,并要求接收方继续履行本政策和法律规定的义务;接收方变更原处理目的或方式的,应当依法重新取得同意。',
|
||||
'4. 我们原则上不公开披露个人信息。确需公开披露的,将告知披露目的、方式和信息种类,依法取得单独同意并采取相应保护措施,法律另有规定的除外。',
|
||||
'<strong>六、个人信息的保存和安全保护</strong>',
|
||||
'1. 我们按照实现处理目的所必要的最短时间保存个人信息;法律法规、监管规定或双方合同另有要求的,从其规定。保存期限届满后,我们将删除个人信息或进行匿名化处理;因技术原因暂时无法删除的,将停止除存储和采取必要安全保护措施之外的处理。',
|
||||
'2. 我们在中华人民共和国境内收集和产生的个人信息原则上存储在境内。具体产品的数据中心和用户业务数据存储位置以产品说明、订单或合同为准。',
|
||||
'3. 我们根据个人信息的种类、处理目的和风险采取与之相适应的安全措施,包括权限控制、身份认证、传输和存储保护、日志审计、备份恢复、人员管理及安全事件处置。互联网环境并非绝对安全,请您妥善保管账号和认证凭证,不要通过不安全渠道发送敏感信息。',
|
||||
'4. 发生或者可能发生个人信息泄露、篡改、丢失时,我们将立即采取补救措施,并依照法律规定向主管部门报告;可能对您的权益造成危害的,我们将依法告知事件情况、可能影响、已采取或拟采取的措施及降低风险的建议。',
|
||||
'<strong>七、您的权利</strong>',
|
||||
'1. 在法律规定范围内,您有权知情、决定、限制或拒绝我们处理您的个人信息,并可请求查阅、复制、更正、补充或删除个人信息,撤回同意、注销账号,以及要求我们解释个人信息处理规则。我们利用个人信息进行自动化决策并对您的权益产生重大影响的,您有权要求说明,并有权拒绝仅通过自动化决策作出的决定。',
|
||||
'2. 您可以通过产品提供的账号设置、工单或本政策所列联系方式提出请求。为保护账号和信息安全,我们可能验证您的身份,并在法律规定的期限内处理。请求明显不合理、超出必要限度,或法律规定可以不予响应的,我们可能拒绝或限制响应,并向您说明理由。',
|
||||
'3. 账号注销后,我们将停止提供与账号相关的服务,并依法删除或匿名化处理相关个人信息;依法需要保留的信息,在保存期间仅用于履行法定义务或争议处理。注销前,请您按照产品说明妥善迁移或备份用户业务数据。',
|
||||
'<strong>八、未成年人个人信息</strong>',
|
||||
'不以不满十四周岁的未成年人应当在父母或其他监护人指导下使用开元云相关服务。我们发现未经监护人同意处理了不满十四周岁未成年人的个人信息时,将依法删除或采取其他必要措施。',
|
||||
'<strong>九、个人信息跨境提供</strong>',
|
||||
'1. 如特定服务需要向中华人民共和国境外提供个人信息,我们将仅在业务确有必要且符合法律规定的情况下进行,并在提供前告知境外接收方信息、处理目的和方式、个人信息种类以及您向境外接收方行使权利的方式和程序。我们将依法履行相应程序,取得单独同意,并要求境外接收方达到法律规定的个人信息保护标准。',
|
||||
'2. 用户自行选择境外资源、境外模型或其他境外服务的,相关数据位置和跨境安排以对应产品说明、订单、合同及单独告知为准。',
|
||||
'<strong>十、本政策的更新</strong>',
|
||||
'1. 我们可能根据法律变化、业务调整或个人信息处理活动变化更新本政策。更新后的政策将通过网站、产品页面或其他适当方式发布。处理目的、方式、个人信息种类、保存期限或个人权利等发生重大变化的,我们将以显著方式通知;依法需要重新取得同意的,将在相关处理前完成。',
|
||||
'2. 未经您的同意,我们不会通过更新本政策减损您依法享有的权利。',
|
||||
'<strong>十一、如何联系我们</strong>',
|
||||
'1. 如您对本政策或个人信息处理活动有疑问、意见、投诉,或需要行使个人信息权利,可通过以下方式联系开元云:',
|
||||
'公司名称:【开元云(北京)科技有限公司】',
|
||||
'联系地址:【北京市朝阳区东三环中路65号富力中心】',
|
||||
'联系电话:【400-6150805 010-65917875】',
|
||||
'联系邮箱:【Open-computing@kaiyuancloud.cn】',
|
||||
'2. 我们将在核验您的身份后依法处理。对处理结果有异议的,您可以再次向我们反馈,也可以依法向履行个人信息保护职责的部门投诉、举报或寻求其他救济。'
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -57,6 +57,14 @@
|
||||
<button v-if="!isNcmatchDomain" type="button" class="nav-item" @click.stop="goHomeAnchor('news')">
|
||||
{{ $t('topbar.news') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="nav-item"
|
||||
:class="{ active: $route.path.includes('/homePage/about') }"
|
||||
@click.stop="navigateTo('/homePage/about')"
|
||||
>
|
||||
{{ aboutUsText }}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="user-actions">
|
||||
@ -275,6 +283,7 @@ export default Vue.extend({
|
||||
activeLocale: 'zh-CN',
|
||||
isProductPanelVisible: false,
|
||||
loginDialogVisible: false,
|
||||
consoleRoutesEnsured: false,
|
||||
|
||||
// 登录信息
|
||||
isShowKbossCharge: false,
|
||||
@ -296,7 +305,7 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['sidebar', 'avatar', 'device']),
|
||||
...mapGetters(['sidebar', 'avatar', 'device', 'permission_routes']),
|
||||
...mapState({
|
||||
mybalance: state => state.user.mybalance,
|
||||
logoutUrl: state => state.login.logoutUrl,
|
||||
@ -313,7 +322,7 @@ export default Vue.extend({
|
||||
return orgType !== '2' && orgType !== '3' && userId === null
|
||||
},
|
||||
isNcmatchDomain() {
|
||||
return window.location.hostname.includes('ncmatch.cn')
|
||||
return window.location.hostname.includes('ncmatch.cn') || window.location.hostname.includes('zgcopc.opencomputing.cn')
|
||||
},
|
||||
isActiveHome() {
|
||||
return this.isNcmatchDomain
|
||||
@ -326,6 +335,9 @@ export default Vue.extend({
|
||||
langToggleTitle() {
|
||||
return this.activeLocale === 'en-US' ? 'Switch to Chinese' : '切换到英文'
|
||||
},
|
||||
aboutUsText() {
|
||||
return this.activeLocale === 'en-US' ? 'About Us' : '关于我们'
|
||||
},
|
||||
localizedProductMenuItems() {
|
||||
const isEn = this.activeLocale === 'en-US'
|
||||
const labels = {
|
||||
@ -338,7 +350,7 @@ export default Vue.extend({
|
||||
supplySquare: isEn ? 'Supply Square' : '供需广场',
|
||||
// about: isEn ? 'About Us' : '关于我们'
|
||||
}
|
||||
const hiddenNcmatchActions = ['computeMarket', 'trainPlatform', 'agentStore']
|
||||
const hiddenNcmatchActions = ['trainPlatform', 'agentStore']
|
||||
return this.productMenuItems
|
||||
.filter(item => !this.isNcmatchDomain || !hiddenNcmatchActions.includes(item.action))
|
||||
.map(item => ({ ...item, label: labels[item.action] || item.action }))
|
||||
@ -487,7 +499,7 @@ export default Vue.extend({
|
||||
if (yuanJingWindow) yuanJingWindow.close()
|
||||
this.$message.error((res && res.msg) || '获取元境授权参数失败')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const loginUrl = `${yuanJingBaseUrl}/#/getCookie?deerer=${encodeURIComponent(deerer)}`
|
||||
if (yuanJingWindow) yuanJingWindow.location.href = loginUrl
|
||||
@ -519,6 +531,7 @@ export default Vue.extend({
|
||||
this.loginDialogVisible = false
|
||||
this.nick_name = sessionStorage.getItem('username') || ''
|
||||
this.userId = sessionStorage.getItem('userId')
|
||||
this.consoleRoutesEnsured = false
|
||||
},
|
||||
goLogin() {
|
||||
this.openLoginDialog()
|
||||
@ -537,9 +550,35 @@ export default Vue.extend({
|
||||
handleUnreadCountUpdate(count) {
|
||||
this.messageCount = count
|
||||
},
|
||||
goB() {
|
||||
async ensureConsoleRoutes() {
|
||||
if (this.consoleRoutesEnsured) return
|
||||
const authsText = sessionStorage.getItem('auths') || '[]'
|
||||
let auths = []
|
||||
try {
|
||||
auths = JSON.parse(authsText)
|
||||
} catch (error) {
|
||||
auths = []
|
||||
}
|
||||
const rolesText = sessionStorage.getItem('roles') || '[]'
|
||||
let roles = []
|
||||
try {
|
||||
roles = JSON.parse(rolesText)
|
||||
} catch (error) {
|
||||
roles = []
|
||||
}
|
||||
const accessRoutes = await this.$store.dispatch('permission/generateRoutes', {
|
||||
user: sessionStorage.getItem('username') || '',
|
||||
auths,
|
||||
orgType: sessionStorage.getItem('org_type'),
|
||||
roles
|
||||
})
|
||||
this.$router.addRoutes(accessRoutes)
|
||||
this.consoleRoutesEnsured = true
|
||||
},
|
||||
async goB() {
|
||||
await this.ensureConsoleRoutes()
|
||||
const role = sessionStorage.getItem('jueseNew') || ''
|
||||
if (role.includes('客户')) this.$router.push('/product/productHome')
|
||||
if (role.includes('客户')) this.$router.push('/product/productHome').catch(() => this.$router.replace('/product/productHome'))
|
||||
else if (role.includes('运营')) this.$router.push('/operation/supplierManagement')
|
||||
else if (role.includes('运维')) this.$router.push('/operationAndMaintenance/workOrderProcessing')
|
||||
else if (role.includes('销售')) this.$router.push('/sales/distributorManagement')
|
||||
@ -606,7 +645,19 @@ export default Vue.extend({
|
||||
getLogoAPI(params).then((res) => {
|
||||
if (res.status === true && res.data && res.data.length) {
|
||||
const info = res.data[0]
|
||||
this.$store.commit('setLogoInfoNew', info.additional_msg)
|
||||
const additionalMsg = info.additional_msg || {}
|
||||
const home = { ...(additionalMsg.home || {}) }
|
||||
|
||||
home.mobile = home.mobile || info.mobile || ''
|
||||
home.email = home.email || info.email || ''
|
||||
home.address_zh = home.address_zh || home.address || info.address || home.adress || ''
|
||||
home.address_en = home.address_en || home.address_zh || home.address || info.address || home.adress || ''
|
||||
home.adress = home.address_zh
|
||||
|
||||
this.$store.commit('setLogoInfoNew', {
|
||||
...additionalMsg,
|
||||
home
|
||||
})
|
||||
if (info.orgname !== '业主机构') {
|
||||
this.$store.commit('setLogo', info.logo)
|
||||
} else {
|
||||
|
||||
@ -264,7 +264,7 @@ export default {
|
||||
return
|
||||
}
|
||||
this.$message.success('实例创建请求已提交')
|
||||
this.$router.push('/containerInstance/index')
|
||||
this.$router.push('/homePage/computeMarket')
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -273,7 +273,7 @@ export default {
|
||||
<style scoped lang="scss">
|
||||
.create-instance-page {
|
||||
min-height: 100vh;
|
||||
padding: 28px 0 88px;
|
||||
padding: 82px 0 88px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(30, 111, 255, 0.12), transparent 30%),
|
||||
linear-gradient(180deg, #f7fbff 0%, #f5f7fb 100%);
|
||||
@ -704,4 +704,10 @@ aside{
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.create-instance-page {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,37 +1,37 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog
|
||||
title="联系销售"
|
||||
:title="$t('contactSales.title')"
|
||||
:visible="dialogVisible"
|
||||
width="400px"
|
||||
center
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
@close="cancelBtn"
|
||||
@update:visible="handleDialogVisibleUpdate"
|
||||
top="8vh"
|
||||
custom-class="talk-dialog"
|
||||
@close="cancelBtn"
|
||||
@update:visible="handleDialogVisibleUpdate"
|
||||
>
|
||||
<div class="talk-body">
|
||||
<div class="talk-form">
|
||||
<el-form ref="ruleForm" :rules="rules" label-position="top" label-width="80px" :model="addData">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="1. 请填写您的姓名" prop="name">
|
||||
<el-input v-model.trim="addData.name" maxlength="20" placeholder="请输入您的姓名"></el-input>
|
||||
<el-form-item :label="$t('contactSales.nameLabel')" prop="name">
|
||||
<el-input v-model.trim="addData.name" maxlength="20" :placeholder="$t('contactSales.namePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="phone" label="2. 请填写您的联系方式">
|
||||
<el-input v-model.trim="addData.phone" maxlength="11" placeholder="请输入您的联系电话"></el-input>
|
||||
<el-form-item prop="phone" :label="$t('contactSales.phoneLabel')">
|
||||
<el-input v-model.trim="addData.phone" maxlength="11" :placeholder="$t('contactSales.phonePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item label="3. 请填写您的邮箱" prop="email" class="form-item--compact">
|
||||
<el-input v-model.trim="addData.email" maxlength="60" placeholder="请输入您的邮箱"></el-input>
|
||||
<el-form-item :label="$t('contactSales.emailLabel')" prop="email" class="form-item--compact">
|
||||
<el-input v-model.trim="addData.email" maxlength="60" :placeholder="$t('contactSales.emailPlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item label="4. 请填写您的公司名称" prop="company" class="form-item--compact">
|
||||
<el-input v-model.trim="addData.company" maxlength="80" placeholder="请输入您的公司名称"></el-input>
|
||||
<el-form-item :label="$t('contactSales.companyLabel')" prop="company" class="form-item--compact">
|
||||
<el-input v-model.trim="addData.company" maxlength="80" :placeholder="$t('contactSales.companyPlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item label="5. 请选择您的企业类型(单选)" prop="enterprise_type" class="form-item--full">
|
||||
<el-form-item :label="$t('contactSales.enterpriseLabel')" prop="enterprise_type" class="form-item--full">
|
||||
<el-radio-group v-model="addData.enterprise_type" class="option-grid">
|
||||
<el-radio
|
||||
v-for="item in enterpriseOptions"
|
||||
v-for="item in localizedEnterpriseOptions"
|
||||
:key="item.id"
|
||||
:label="item.id"
|
||||
class="option-item"
|
||||
@ -40,63 +40,71 @@
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="6. 请选择您所在的省份(单选)" prop="region" class="form-item--full">
|
||||
<el-form-item :label="$t('contactSales.provinceLabel')" prop="region" class="form-item--full">
|
||||
<el-select
|
||||
:key="provinceSelectKey"
|
||||
v-model="addData.region"
|
||||
filterable
|
||||
placeholder="输入省份名称或拼音首字母搜索..."
|
||||
:placeholder="$t('contactSales.provincePlaceholder')"
|
||||
class="full-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in provinceOptions"
|
||||
v-for="item in localizedProvinceOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="7. 如果您有其他问题需要咨询,请留言" class="form-item--message">
|
||||
<el-form-item :label="$t('contactSales.messageLabel')" class="form-item--message">
|
||||
<el-input
|
||||
v-model.trim="addData.content"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入您想咨询的内容..."
|
||||
></el-input>
|
||||
:placeholder="$t('contactSales.messagePlaceholder')"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<el-checkbox class="agreement-checkbox" v-model="checked">
|
||||
您填写的信息仅用于本次业务对接沟通,公司将严格落实信息安全保护机制,不泄露、不滥用您的任何个人资料。
|
||||
<el-checkbox v-model="checked" class="agreement-checkbox">
|
||||
{{ $t('contactSales.privacy') }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<div v-if="qrCodeUrl" class="qcode">
|
||||
<img :src="qrCodeUrl" alt="官方客服二维码">
|
||||
<span>扫码添加官方客服</span>
|
||||
<span>{{ $t('contactSales.qrcode') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button class="cancel-btn" @click="cancelBtn">取消</el-button>
|
||||
<el-button class="submit-btn" type="primary" :loading="addBtnLoading" @click="confirmBtn">提交咨询</el-button>
|
||||
<el-button class="cancel-btn" @click="cancelBtn">{{ $t('contactSales.cancel') }}</el-button>
|
||||
<el-button class="submit-btn" type="primary" :loading="addBtnLoading" @click="confirmBtn">{{ $t('contactSales.submit') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import {mapState} from "vuex";
|
||||
import {reqNewHomeConsult} from "@/api/newHome";
|
||||
import {reqConsultForm} from "@/api/H5";
|
||||
import { mapState } from 'vuex'
|
||||
import { reqNewHomeConsult } from '@/api/newHome'
|
||||
import { reqConsultForm } from '@/api/H5'
|
||||
import {
|
||||
extractConsultOptionList,
|
||||
isEnglishLocale,
|
||||
localizeDictOptions,
|
||||
localizeDirectionOptions,
|
||||
normalizeDictOptions as buildDictOptions
|
||||
} from '@/utils/consultDict'
|
||||
|
||||
export default Vue.extend({
|
||||
name: "talk",
|
||||
name: 'Talk',
|
||||
data() {
|
||||
const validatePhone = (rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback(new Error('请输入联系电话'))
|
||||
callback(new Error(this.$t('contactSales.phonePlaceholder')))
|
||||
} else if (!/^1[3-9]\d{9}$/.test(value)) {
|
||||
callback(new Error('请输入正确的手机号'))
|
||||
callback(new Error(this.$t('contactSales.phoneInvalid') || '请输入正确的手机号'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
@ -105,23 +113,23 @@ export default Vue.extend({
|
||||
return {
|
||||
rules: {
|
||||
name: [
|
||||
{required: true, message: '请输入姓名', trigger: 'blur'},
|
||||
{ required: true, message: this.$t('contactSales.namePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{required: true, validator: validatePhone, trigger: 'blur'}
|
||||
{ required: true, validator: validatePhone, trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{required: true, message: '请输入邮箱', trigger: 'blur'},
|
||||
{type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur'}
|
||||
{ required: true, message: this.$t('contactSales.emailPlaceholder'), trigger: 'blur' },
|
||||
{ type: 'email', message: this.$t('contactSales.emailInvalid') || '请输入正确的邮箱地址', trigger: 'blur' }
|
||||
],
|
||||
company: [
|
||||
{required: true, message: '请输入公司名称', trigger: 'blur'}
|
||||
{ required: true, message: this.$t('contactSales.companyPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
enterprise_type: [
|
||||
{required: true, message: '请选择企业类型', trigger: 'change'}
|
||||
{ required: true, message: this.$t('contactSales.enterpriseRequired') || '请选择企业类型', trigger: 'change' }
|
||||
],
|
||||
region: [
|
||||
{required: true, message: '请选择所在省份', trigger: 'change'}
|
||||
{ required: true, message: this.$t('contactSales.provinceRequired') || '请选择所在省份', trigger: 'change' }
|
||||
]
|
||||
},
|
||||
addBtnLoading: false,
|
||||
@ -129,15 +137,16 @@ export default Vue.extend({
|
||||
dialogVisible: false,
|
||||
enterpriseOptions: [],
|
||||
provinceOptions: [],
|
||||
provinceSelectKey: 0,
|
||||
addData: {
|
||||
content: '',//需求内容
|
||||
custom_type: "1",//客户类型 0-个人 1-企业
|
||||
name: "",//姓名
|
||||
phone: "",//手机号
|
||||
company: "",//公司名称
|
||||
email: "",//邮箱
|
||||
content: '', // 需求内容
|
||||
custom_type: '1', // 客户类型 0-个人 1-企业
|
||||
name: '', // 姓名
|
||||
phone: '', // 手机号
|
||||
company: '', // 公司名称
|
||||
email: '', // 邮箱
|
||||
enterprise_type: '',
|
||||
region: '',
|
||||
region: ''
|
||||
},
|
||||
labelPosition: 'right',
|
||||
formLabelAlign: {
|
||||
@ -147,7 +156,27 @@ export default Vue.extend({
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
showTalk: (state) => state.product.showTalk,
|
||||
logoInfoNew: state => state.product.logoInfoNew
|
||||
}),
|
||||
qrCodeUrl() {
|
||||
return this.logoInfoNew && this.logoInfoNew.home && this.logoInfoNew.home.qrCode
|
||||
? this.logoInfoNew.home.qrCode
|
||||
: ''
|
||||
},
|
||||
localizedEnterpriseOptions() {
|
||||
return localizeDictOptions(this.enterpriseOptions, isEnglishLocale(this.$i18n))
|
||||
},
|
||||
localizedProvinceOptions() {
|
||||
return localizeDictOptions(this.provinceOptions, isEnglishLocale(this.$i18n))
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$i18n.locale'() {
|
||||
this.provinceSelectKey += 1
|
||||
},
|
||||
showTalk: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
@ -168,52 +197,41 @@ export default Vue.extend({
|
||||
company: '',
|
||||
email: '',
|
||||
enterprise_type: '',
|
||||
region: '',
|
||||
region: ''
|
||||
}
|
||||
},
|
||||
normalizeDictOptions(list, type) {
|
||||
return list
|
||||
.filter(item => item && item.dict_type === type)
|
||||
.sort((a, b) => Number(a.sort_order || 0) - Number(b.sort_order || 0))
|
||||
.map(item => ({
|
||||
id: item.dict_key,
|
||||
name: item.dict_value
|
||||
}))
|
||||
},
|
||||
extractConsultOptionList(res) {
|
||||
if (res && Array.isArray(res.data)) return res.data
|
||||
if (res && res.data && Array.isArray(res.data.data)) return res.data.data
|
||||
return []
|
||||
},
|
||||
async fetchConsultOptions() {
|
||||
try {
|
||||
const res = await reqConsultForm()
|
||||
const list = this.extractConsultOptionList(res)
|
||||
const list = extractConsultOptionList(res)
|
||||
if (!list.length) return
|
||||
|
||||
const enterpriseOptions = this.normalizeDictOptions(list, 'enterprise_type')
|
||||
const provinceOptions = this.normalizeDictOptions(list, 'region')
|
||||
const enterpriseOptions = buildDictOptions(list, 'enterprise_type')
|
||||
const provinceOptions = buildDictOptions(list, 'region')
|
||||
|
||||
if (enterpriseOptions.length) this.enterpriseOptions = enterpriseOptions
|
||||
if (provinceOptions.length) this.provinceOptions = provinceOptions
|
||||
if (provinceOptions.length) {
|
||||
this.provinceOptions = provinceOptions
|
||||
this.provinceSelectKey += 1
|
||||
}
|
||||
} catch (error) {
|
||||
// 字典接口异常时保持当前选项,避免影响基础提交。
|
||||
}
|
||||
},
|
||||
handleDialogVisibleUpdate(value) {
|
||||
this.dialogVisible = value;
|
||||
this.dialogVisible = value
|
||||
if (!value) {
|
||||
this.$store.commit('setShowTalk', false);
|
||||
this.$store.commit('setShowTalk', false)
|
||||
}
|
||||
},
|
||||
cancelBtn() {
|
||||
this.dialogVisible = false;
|
||||
this.$store.commit('setShowTalk', false);
|
||||
this.dialogVisible = false
|
||||
this.$store.commit('setShowTalk', false)
|
||||
},
|
||||
confirmBtn() {
|
||||
if (!this.checked) {
|
||||
this.$message.warning('请勾选同意协议后再提交!');
|
||||
return;
|
||||
this.$message.warning(this.$t('contactSales.privacyRequired') || '请勾选同意协议后再提交!')
|
||||
return
|
||||
}
|
||||
this.$refs['ruleForm'].validate((valid) => {
|
||||
if (valid) {
|
||||
@ -229,41 +247,28 @@ export default Vue.extend({
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: '感谢您关注人工智能服务平台,我们将尽快联系您!~'
|
||||
});
|
||||
this.$store.commit('setShowTalk', false);
|
||||
})
|
||||
this.$store.commit('setShowTalk', false)
|
||||
this.addData = this.getDefaultFormData()
|
||||
this.checked = false
|
||||
} else {
|
||||
this.$message.error(response.msg || '提交失败,请稍后再试!');
|
||||
this.$message.error(response.msg || '提交失败,请稍后再试!')
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("提交咨询失败:", error);
|
||||
this.$message.error('提交失败,请稍后再试!');
|
||||
console.error('提交咨询失败:', error)
|
||||
this.$message.error('提交失败,请稍后再试!')
|
||||
})
|
||||
} else {
|
||||
this.$message.error('请完善表单信息~')
|
||||
return false;
|
||||
this.$message.error(this.$t('contactSales.formInvalid') || '请完善表单信息~')
|
||||
return false
|
||||
}
|
||||
});
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
showTalk: (state) => state.product.showTalk,
|
||||
logoInfoNew: state => state.product.logoInfoNew,
|
||||
}),
|
||||
qrCodeUrl() {
|
||||
return this.logoInfoNew && this.logoInfoNew.home && this.logoInfoNew.home.qrCode
|
||||
? this.logoInfoNew.home.qrCode
|
||||
: ''
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
::v-deep .talk-dialog {
|
||||
border-radius: 22px;
|
||||
@ -376,7 +381,21 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
.form-item--message {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::v-deep .form-item--message .el-form-item__label {
|
||||
width: 100% !important;
|
||||
float: none;
|
||||
line-height: 1.5;
|
||||
white-space: normal;
|
||||
word-break: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
::v-deep .form-item--message .el-form-item__content {
|
||||
width: calc(100% - 184px);
|
||||
max-width: calc(100% - 184px);
|
||||
}
|
||||
|
||||
.full-select {
|
||||
@ -520,6 +539,11 @@ export default Vue.extend({
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::v-deep .form-item--message .el-form-item__content {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.agreement-checkbox {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
<h4 class="footer-col-title">{{ $t('home.footerProducts') }}</h4>
|
||||
<ul class="footer-link-list">
|
||||
<li
|
||||
v-for="item in footerProductServices"
|
||||
v-for="item in localizedFooterProductServices"
|
||||
:key="item.label"
|
||||
class="footer-link-item"
|
||||
:class="{ clickable: !!item.path }"
|
||||
@ -38,9 +38,18 @@
|
||||
<div class="footer-contact-col">
|
||||
<h4 class="footer-col-title">{{ $t('home.footerContact') }}</h4>
|
||||
<ul class="footer-contact-list">
|
||||
<li>地址:<span>{{ footerHomeInfo.adress }}</span></li>
|
||||
<li>邮箱:<span>{{ footerHomeInfo.email }}</span></li>
|
||||
<li>电话:<span>{{ footerHomeInfo.mobile }}</span></li>
|
||||
<li>
|
||||
<span class="footer-contact-label">{{ $t('home.footerAddress') }}:</span>
|
||||
<span class="footer-contact-value">{{ localizedFooterAddress }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="footer-contact-label">{{ $t('home.footerEmail') }}:</span>
|
||||
<span class="footer-contact-value">{{ footerHomeInfo.email }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="footer-contact-label">{{ $t('home.footerTel') }}:</span>
|
||||
<span class="footer-contact-value">{{ localizedFooterMobile }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="showFooterQrcode" class="footer-qrcode-row">
|
||||
<div class="qr-box">
|
||||
@ -53,7 +62,7 @@
|
||||
<div class="qr-code">
|
||||
<img src="./img/kefu.jpg" alt="">
|
||||
</div>
|
||||
<span class="qr-content">关注公众号</span>
|
||||
<span class="qr-content">{{ $t('home.followOfficialAccount') }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -71,7 +80,7 @@
|
||||
}}
|
||||
<span style="padding: 4px;"></span>
|
||||
</span> {{
|
||||
footerHomeInfo.footerTitle
|
||||
localizedFooterTitle
|
||||
}} {{
|
||||
footerHomeInfo.copyright
|
||||
}} </span>
|
||||
@ -133,13 +142,13 @@ export default Vue.extend({
|
||||
}
|
||||
],
|
||||
footerProductServices: [
|
||||
{ label: 'Token市集', path: '/homePage/tokenMarket' },
|
||||
{ label: '算力市场', path: '/homePage/computeMarket' },
|
||||
{ label: 'OPC公共服务平台', path: '/homePage/opc' },
|
||||
{ label: '训推平台', path: '/homePage/trainPlatform' },
|
||||
{ label: '智能体商店', path: '/homePage/agentStore' },
|
||||
{ label: '供需广场', path: '/ncmatchHome/supplyAndDemandSquare' },
|
||||
{ label: '文档中心', path: '/homePage/new' },
|
||||
{ action: 'tokenMarket', label: 'Token市集', path: '/homePage/tokenMarket' },
|
||||
{ action: 'computeMarket', label: '算力市场', path: '/homePage/computeMarket' },
|
||||
{ action: 'opc', label: 'OPC公共服务平台', path: '/homePage/opc' },
|
||||
{ action: 'yuanjing', label: '元境', path: '/homePage/yuanJing' },
|
||||
{ action: 'trainPlatform', label: '训推平台', path: '/homePage/trainPlatform' },
|
||||
{ action: 'agentStore', label: '智能体商店', path: '/homePage/agentStore' },
|
||||
{ action: 'supplySquare', label: '供需广场', path: '/ncmatchHome/supplyAndDemandSquare' },
|
||||
],
|
||||
footerAboutUs: [
|
||||
{ label: '关于我们', path: '/homePage/about' },
|
||||
@ -190,9 +199,49 @@ export default Vue.extend({
|
||||
footerHomeInfo() {
|
||||
return this.hasLogoInfo ? this.logoInfoNew.home : {}
|
||||
},
|
||||
isEnglishLocale() {
|
||||
return !!(this.$i18n && this.$i18n.locale === 'en-US')
|
||||
},
|
||||
localizedFooterAddress() {
|
||||
const info = this.footerHomeInfo
|
||||
if (!info || !Object.keys(info).length) return ''
|
||||
|
||||
if (this.isEnglishLocale) {
|
||||
return info.address_en || info.address || info.adress || info.address_zh || ''
|
||||
}
|
||||
return info.address_zh || info.address || info.adress || info.address_en || ''
|
||||
},
|
||||
localizedFooterMobile() {
|
||||
const info = this.footerHomeInfo
|
||||
return (info && (info.mobile || info.tel || info.phone)) || ''
|
||||
},
|
||||
localizedFooterTitle() {
|
||||
const info = this.footerHomeInfo
|
||||
if (!info || !Object.keys(info).length) return ''
|
||||
|
||||
if (this.isEnglishLocale) {
|
||||
return info.footerTitle_en || info.footerTitleEn || info.footerTitle || ''
|
||||
}
|
||||
return info.footerTitle || info.footerTitle_zh || ''
|
||||
},
|
||||
showFooterQrcode() {
|
||||
return this.hasLogoInfo && this.footerHomeInfo.bannerTitle !== '开元数智'
|
||||
},
|
||||
localizedFooterProductServices() {
|
||||
const labels = {
|
||||
opc: 'OPC',
|
||||
computeMarket: this.isEnglishLocale ? 'Compute Market' : '算力市场',
|
||||
tokenMarket: this.isEnglishLocale ? 'Token Market' : 'Token市集',
|
||||
yuanjing: this.isEnglishLocale ? 'Yuanjing' : '元境',
|
||||
trainPlatform: this.isEnglishLocale ? 'Train & Infer' : '训推平台',
|
||||
agentStore: this.isEnglishLocale ? 'Agent Store' : '智能体商店',
|
||||
supplySquare: this.isEnglishLocale ? 'Supply Square' : '供需广场'
|
||||
}
|
||||
return this.footerProductServices.map(item => ({
|
||||
...item,
|
||||
label: labels[item.action] || item.label
|
||||
}))
|
||||
},
|
||||
username() {
|
||||
return sessionStorage.getItem('username') || '';
|
||||
},
|
||||
@ -391,10 +440,28 @@ export default Vue.extend({
|
||||
list-style: none;
|
||||
color: #5d6477;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
line-height: 1.75;
|
||||
|
||||
li {
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.footer-contact-label {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.footer-contact-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -359,7 +359,7 @@ export default Vue.extend({
|
||||
}
|
||||
})
|
||||
} else {
|
||||
window.dispatchEvent(new Event('kboss-open-login-dialog'))
|
||||
this.$router.push('/login')
|
||||
}
|
||||
},
|
||||
initData() {
|
||||
@ -418,14 +418,14 @@ export default Vue.extend({
|
||||
if (this.loginState) {
|
||||
this.$router.push('/ncmatchHome/favoriteBox')
|
||||
} else {
|
||||
window.dispatchEvent(new Event('kboss-open-login-dialog'))
|
||||
this.$router.push('/login')
|
||||
}
|
||||
},
|
||||
goHistory() {
|
||||
if (this.loginState) {
|
||||
this.$router.push('/ncmatchHome/historyBox')
|
||||
} else {
|
||||
window.dispatchEvent(new Event('kboss-open-login-dialog'))
|
||||
this.$router.push('/login')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,10 +8,10 @@
|
||||
<section class="home-hero">
|
||||
<div class="home-hero__inner animate-in">
|
||||
<div class="hero-title-row">
|
||||
<h1>{{ $t('home.heroTitle') }}</h1>
|
||||
<h1 :class="{ 'home-hero__title--english': isEnglish }">{{ $t('home.heroTitle') }}</h1>
|
||||
</div>
|
||||
<h2 v-if="$t('home.heroSubtitle')">{{ $t('home.heroSubtitle') }}</h2>
|
||||
<p>{{ $t('home.heroSlogan') }}</p>
|
||||
<!-- <p>{{ $t('home.heroSlogan') }}</p> -->
|
||||
<div class="hero-actions">
|
||||
<button type="button" class="use-btn" @click="goSolution">{{ $t('home.solutionsBtn') }}</button>
|
||||
<div class="outline-btn" @click="contactSales">{{ $t('home.contactSalesBtn') }}</div>
|
||||
@ -37,6 +37,14 @@
|
||||
@mouseleave="startSolutionCarousel"
|
||||
@click="setSolutionIndex(item.index)"
|
||||
>
|
||||
<button
|
||||
v-if="item.card.scenarioPath"
|
||||
type="button"
|
||||
class="solution-card__scenario-link"
|
||||
@click.stop="goSolutionScenario(item.card.scenarioPath)"
|
||||
>
|
||||
{{ $i18n && $i18n.locale === 'en-US' ? 'Scenarios→' : item.card.scenarioLabel }}
|
||||
</button>
|
||||
<div class="solution-card__front">
|
||||
<div class="solution-card__icon" v-html="item.card.icon"></div>
|
||||
<h3>{{ item.card.title }}</h3>
|
||||
@ -71,7 +79,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 成功案例 -->
|
||||
<!-- 成功案例 -->
|
||||
<section id="cases-section" class="case-section">
|
||||
<div class="case-inner">
|
||||
<div class="case-head">
|
||||
@ -101,7 +109,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="case-cta" :style="{ backgroundImage: `url(${caseCtaBg})` }">
|
||||
@ -129,7 +137,7 @@
|
||||
<div class="news-list">
|
||||
<article
|
||||
v-for="(item, index) in localizedNewsList"
|
||||
:key="item.title"
|
||||
:key="item.id"
|
||||
class="news-card"
|
||||
:class="{ 'news-card-active': index === activeNewsIndex }"
|
||||
@mouseenter="activeNewsIndex = index"
|
||||
@ -213,6 +221,8 @@ export default {
|
||||
],
|
||||
bg: 'linear-gradient(145deg, rgba(208, 236, 249, 0.72) 0%, rgba(168, 216, 244, 0.64) 44%, rgba(124, 196, 238, 0.58) 100%)',
|
||||
shadow: '0 18px 54px rgba(80, 150, 220, 0.2), 0 4px 16px rgba(80, 150, 220, 0.08)',
|
||||
scenarioLabel: '投策智能体→',
|
||||
scenarioPath: '/homePage/agentStore/decisionCase',
|
||||
icon: '<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="24" cy="18" r="6"/><circle cx="10" cy="34" r="5"/><circle cx="38" cy="34" r="5"/><line x1="20" y1="23" x2="13" y2="30"/><line x1="28" y1="23" x2="35" y2="30"/><circle cx="36" cy="14" r="3.5"/><circle cx="12" cy="14" r="3.5"/></svg>'
|
||||
},
|
||||
{
|
||||
@ -303,6 +313,8 @@ export default {
|
||||
],
|
||||
bg: 'linear-gradient(145deg, rgba(200, 214, 242, 0.72) 0%, rgba(158, 180, 230, 0.64) 44%, rgba(116, 146, 216, 0.58) 100%)',
|
||||
shadow: '0 18px 54px rgba(60, 80, 160, 0.18), 0 4px 16px rgba(60, 80, 160, 0.08)',
|
||||
scenarioLabel: '合同智能审查→',
|
||||
scenarioPath: '/homePage/agentStore/contractCase',
|
||||
icon: '<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 6h16l10 10v26a4 4 0 0 1-4 4H12a4 4 0 0 1-4-4V10a4 4 0 0 1 4-4z"/><polyline points="28,6 28,16 38,16"/><line x1="14" y1="24" x2="34" y2="24"/><line x1="14" y1="31" x2="30" y2="31"/></svg>'
|
||||
},
|
||||
{
|
||||
@ -327,6 +339,9 @@ export default {
|
||||
showTalkVisible() {
|
||||
return this.$store.state.product.showTalk
|
||||
},
|
||||
isEnglish() {
|
||||
return this.$i18n && this.$i18n.locale === 'en-US'
|
||||
},
|
||||
localizedSolutionCards() {
|
||||
const keyMap = {
|
||||
'群智协作': 'collaboration',
|
||||
@ -378,7 +393,29 @@ export default {
|
||||
})
|
||||
},
|
||||
localizedNewsList() {
|
||||
return this.newsList
|
||||
const isEnglish = this.$i18n && this.$i18n.locale === 'en-US'
|
||||
return this.newsList.map((item) => {
|
||||
const tag = isEnglish
|
||||
? (item.tagEn || item.tagZh || this.$t('home.news.tag'))
|
||||
: (item.tagZh || item.tagEn || this.$t('home.news.tag'))
|
||||
const title = isEnglish
|
||||
? (item.titleEn || item.titleZh || '')
|
||||
: (item.titleZh || item.titleEn || '')
|
||||
const desc = isEnglish
|
||||
? (item.descEn || item.descZh || '')
|
||||
: (item.descZh || item.descEn || '')
|
||||
const date = isEnglish
|
||||
? (item.dateEn || item.dateZh || '')
|
||||
: (item.dateZh || item.dateEn || '')
|
||||
|
||||
return {
|
||||
...item,
|
||||
tag,
|
||||
title,
|
||||
desc,
|
||||
date
|
||||
}
|
||||
})
|
||||
},
|
||||
visibleSolutionCards() {
|
||||
const slots = [-3, -2, -1, 0, 1, 2, 3]
|
||||
@ -430,6 +467,9 @@ export default {
|
||||
block: 'start'
|
||||
})
|
||||
},
|
||||
goSolutionScenario(path) {
|
||||
this.navigateTo(path)
|
||||
},
|
||||
contactSales() {
|
||||
this.$store.commit('setShowTalk', true)
|
||||
},
|
||||
@ -455,11 +495,15 @@ export default {
|
||||
normalizeHomeNewsItem(row, index) {
|
||||
return {
|
||||
id: row.id,
|
||||
tag: row.article_type || this.$t('home.news.tag'),
|
||||
tagClass: this.getNewsTagClass(index),
|
||||
date: row.publish_time || row.publishTime || '',
|
||||
title: row.title || '',
|
||||
desc: row.summary || row.desc || row.description || ''
|
||||
tagZh: row.article_type_zh || row.article_type || '',
|
||||
tagEn: row.article_type_en || '',
|
||||
dateZh: row.publish_time_zh || row.publish_time || row.publishTime || '',
|
||||
dateEn: row.publish_time_en || '',
|
||||
titleZh: row.title_zh || row.title || '',
|
||||
titleEn: row.title_en || '',
|
||||
descZh: row.summary_zh || row.summary || row.desc || row.description || '',
|
||||
descEn: row.summary_en || ''
|
||||
}
|
||||
},
|
||||
getResponseList(res) {
|
||||
@ -481,8 +525,7 @@ export default {
|
||||
this.newsList = []
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
nextSolution() {
|
||||
this.switchSolution(1)
|
||||
},
|
||||
@ -619,6 +662,12 @@ body.dark-theme .bg-orb {
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.home-hero__title--english {
|
||||
font-size: clamp(40px, 5vw, 58px);
|
||||
letter-spacing: 0.01em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 20px 0 0;
|
||||
font-size: 72px;
|
||||
@ -650,6 +699,13 @@ body.dark-theme .bg-orb {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.home-hero__inner .home-hero__title--english {
|
||||
font-size: clamp(32px, 9vw, 48px);
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -690,7 +746,6 @@ body.dark-theme .bg-orb {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
|
||||
&:hover {
|
||||
background: #fff;
|
||||
transform: translateY(-2px);
|
||||
@ -770,11 +825,13 @@ body.dark-theme .bg-orb {
|
||||
h3 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
margin: 24px 0 8px;
|
||||
font-size: 24px;
|
||||
line-height: 1.3;
|
||||
font-weight: 800;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
span {
|
||||
@ -817,13 +874,48 @@ body.dark-theme .bg-orb {
|
||||
.solution-card__front {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
transition: all 0.28s ease;
|
||||
}
|
||||
|
||||
.solution-card__scenario-link {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 3;
|
||||
padding: 5px 9px;
|
||||
color: #1d4ed8;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
background: rgba(255, 255, 255, 0.58);
|
||||
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 999px;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
pointer-events: none;
|
||||
transform: translateY(-4px);
|
||||
transition: background 0.2s ease, color 0.2s ease, opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.solution-card:hover .solution-card__scenario-link {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.solution-card__scenario-link:hover {
|
||||
color: #1e40af;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.solution-card__icon {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@ -865,15 +957,12 @@ body.dark-theme .bg-orb {
|
||||
transition: all 0.25s ease;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(37, 99, 235, 0.18);
|
||||
border-radius: 999px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
@ -1236,6 +1325,7 @@ body.dark-theme .bg-orb {
|
||||
font-size: 34px;
|
||||
line-height: 1.25;
|
||||
font-weight: 800;
|
||||
text-align: center ;
|
||||
}
|
||||
|
||||
p {
|
||||
@ -1546,8 +1636,6 @@ body.dark-theme .bg-orb {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes heroFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="main-content">
|
||||
<section id="banner" class="public-hero">
|
||||
<section id="banner" class="public-hero">
|
||||
<div class="floating-orb orb-1"></div>
|
||||
<div class="floating-orb orb-2"></div>
|
||||
<div class="floating-orb orb-3"></div>
|
||||
@ -22,7 +22,7 @@
|
||||
</div>
|
||||
|
||||
<div class="hero-inner">
|
||||
<p class="hero-slogan">好用还省钱,Token 就上开元云</p>
|
||||
<p class="hero-slogan">好用还省钱,Token 就上数智开物</p>
|
||||
<h1>数智开物<span>OPC</span>公共服务平台</h1>
|
||||
<p class="hero-subtitle">
|
||||
为 <strong>OPC</strong> 而生,极致性价比一站式模型平台
|
||||
@ -435,7 +435,7 @@ export default Vue.extend({
|
||||
<style scoped lang="scss">
|
||||
.jd-homepage {
|
||||
margin: 0;
|
||||
// padding-top: 92px;
|
||||
padding-top: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(180deg, #f0f7ff 0%, #ffffff 46%, #f7faff 74%, #f8fafc 100%);
|
||||
overflow-x: hidden;
|
||||
@ -457,6 +457,7 @@ export default Vue.extend({
|
||||
.public-hero {
|
||||
width: 100%;
|
||||
min-height: 760px;
|
||||
padding-top: 82px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
@ -1069,7 +1070,7 @@ export default Vue.extend({
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 576px) {
|
||||
.jd-homepage {
|
||||
padding-top: 80px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
@ -1082,6 +1083,7 @@ export default Vue.extend({
|
||||
|
||||
.public-hero {
|
||||
min-height: auto;
|
||||
padding-top: 72px;
|
||||
}
|
||||
|
||||
.public-hero h1 {
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
</div>
|
||||
|
||||
<div class="article-editor" :class="{ 'is-editor-fullscreen': editorFullscreen }">
|
||||
<div class="article-editor__left">
|
||||
<div class="article-editor__left" :class="{ 'has-language-form': showEnglishForm }">
|
||||
<div class="article-editor__form">
|
||||
<el-form :model="form" label-width="76px" size="small">
|
||||
<el-form-item>
|
||||
@ -71,11 +71,27 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="language-trigger">
|
||||
<div class="language-trigger__title">
|
||||
<span class="language-trigger__badge">EN</span>
|
||||
<div>
|
||||
<strong>多语言输入</strong>
|
||||
<small>添加英文版文章内容</small>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="language-trigger__button"
|
||||
:aria-expanded="showEnglishForm.toString()"
|
||||
@click="showEnglishForm = !showEnglishForm"
|
||||
>
|
||||
{{ showEnglishForm ? '收起英文版' : '添加英文版' }}
|
||||
<i :class="showEnglishForm ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="dialogVisible" class="wang-editor-wrap">
|
||||
|
||||
<Toolbar
|
||||
class="wang-toolbar"
|
||||
:editor="editor"
|
||||
@ -90,6 +106,92 @@
|
||||
@onCreated="handleEditorCreated"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section v-if="showEnglishForm" class="article-editor__language-section">
|
||||
<div class="language-section-head">
|
||||
<div>
|
||||
<span class="language-section-head__eyebrow">ENGLISH VERSION</span>
|
||||
<h3>英文版文章</h3>
|
||||
<p>英文内容会与中文内容一并保存或发布。</p>
|
||||
</div>
|
||||
<span class="language-section-head__tag">English</span>
|
||||
</div>
|
||||
|
||||
<div class="language-form-grid">
|
||||
<div class="language-field language-field--wide">
|
||||
<label>Article Title</label>
|
||||
<el-input v-model.trim="english.title" placeholder="Enter article title" />
|
||||
</div>
|
||||
<div class="language-field language-field--wide">
|
||||
<label>Intro</label>
|
||||
<el-input
|
||||
v-model.trim="english.summary"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="120"
|
||||
show-word-limit
|
||||
placeholder="Enter article summary"
|
||||
/>
|
||||
</div>
|
||||
<div class="language-field">
|
||||
<label>Category</label>
|
||||
<el-select v-model="english.category" placeholder="Select category">
|
||||
<el-option label="Corporate News" value="Corporate News" />
|
||||
<el-option label="Product News" value="Product News" />
|
||||
<el-option label="Industry Insights" value="Industry Insights" />
|
||||
<el-option label="Event News" value="Event News" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="language-field">
|
||||
<label>Date</label>
|
||||
<el-date-picker
|
||||
v-model="english.publishTime"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="Select date"
|
||||
/>
|
||||
</div>
|
||||
<div class="language-field language-field--wide">
|
||||
<label>Cover</label>
|
||||
<div class="language-cover">
|
||||
<button type="button" class="language-cover__upload" @click="$refs.englishCoverInput.click()">
|
||||
<i class="el-icon-upload2" />
|
||||
Upload cover
|
||||
</button>
|
||||
<el-input
|
||||
v-model.trim="english.coverUrl"
|
||||
class="language-cover__input"
|
||||
placeholder="Upload an image or paste its URL"
|
||||
/>
|
||||
<span v-if="english.coverName" class="language-cover__name">{{ english.coverName }}</span>
|
||||
<input ref="englishCoverInput" type="file" accept="image/*" class="hidden-input" @change="handleEnglishCoverUpload">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="language-richtext">
|
||||
<div class="language-richtext__head">
|
||||
<span>English Content</span>
|
||||
|
||||
</div>
|
||||
<div v-if="dialogVisible" class="wang-editor-wrap wang-editor-wrap--english">
|
||||
<Toolbar
|
||||
class="wang-toolbar"
|
||||
:editor="englishEditor"
|
||||
:default-config="toolbarConfig"
|
||||
:mode="editorMode"
|
||||
/>
|
||||
<Editor
|
||||
v-model="english.content"
|
||||
class="wang-editor"
|
||||
:default-config="englishEditorConfig"
|
||||
:mode="editorMode"
|
||||
@onCreated="handleEnglishEditorCreated"
|
||||
@onDestroyed="handleEnglishEditorDestroyed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -118,7 +220,7 @@
|
||||
|
||||
<div slot="footer" class="article-editor-footer">
|
||||
<span class="editor-status">{{ dirty ? '未保存' : '已同步' }}</span>
|
||||
<div>
|
||||
<div class="editor-footer-buttons">
|
||||
<el-button size="small" @click="requestClose">取消</el-button>
|
||||
<el-button size="small" @click="handleSave('0')">保存草稿</el-button>
|
||||
<el-button type="primary" size="small" @click="handlePublishConfirm">保存并发布</el-button>
|
||||
@ -157,6 +259,7 @@ export default {
|
||||
dirty: false,
|
||||
// 保存 wangEditor 实例,组件销毁时需要手动释放。
|
||||
editor: null,
|
||||
englishEditor: null,
|
||||
// wangEditor 模式配置。
|
||||
editorMode: 'default',
|
||||
// wangEditor 工具栏配置。
|
||||
@ -166,7 +269,7 @@ export default {
|
||||
placeholder: '请输入文章内容...',
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
customUpload: async (file, insertFn) => {
|
||||
customUpload: async(file, insertFn) => {
|
||||
try {
|
||||
// 正文图片上传到文件接口,正文中保存可回显的图片地址。
|
||||
const url = await this.uploadNewsImage(file)
|
||||
@ -178,13 +281,33 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
englishEditorConfig: {
|
||||
placeholder: 'Please enter article content…',
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
customUpload: async(file, insertFn) => {
|
||||
try {
|
||||
const url = await this.uploadNewsImage(file)
|
||||
insertFn(normalizeImageUrl(url), file.name, normalizeImageUrl(url))
|
||||
} catch (error) {
|
||||
this.$message.error('英文正文图片上传失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// 上传封面的本地预览地址,只用于页面显示,不提交给接口。
|
||||
coverPreview: '',
|
||||
imageUploading: false,
|
||||
englishImageUploading: false,
|
||||
editorFullscreen: false,
|
||||
editorFullscreenObserver: null,
|
||||
// 右侧预览区域宽度。
|
||||
previewWidth: 400,
|
||||
// 控制英文版区域展开状态。
|
||||
showEnglishForm: false,
|
||||
// 英文版内容,与中文字段一起提交到文章保存接口。
|
||||
english: this.createEmptyEnglishForm(),
|
||||
// 标记预览区域是否正在拖拽调整宽度。
|
||||
resizing: false,
|
||||
// 记录拖拽开始时的鼠标位置。
|
||||
@ -230,6 +353,12 @@ export default {
|
||||
// 任意表单字段变化后标记为未保存。
|
||||
this.dirty = true
|
||||
}
|
||||
},
|
||||
english: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.dirty = true
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
@ -238,12 +367,16 @@ export default {
|
||||
this.editor.destroy()
|
||||
this.editor = null
|
||||
}
|
||||
if (this.englishEditor) {
|
||||
this.englishEditor.destroy()
|
||||
this.englishEditor = null
|
||||
}
|
||||
// 清理拖拽监听事件。
|
||||
this.removeResizeListeners()
|
||||
this.stopObserveEditorFullscreen()
|
||||
this.editorFullscreen = false
|
||||
},
|
||||
methods: {
|
||||
methods: {
|
||||
createEmptyForm() {
|
||||
// 创建新增文章时使用的默认表单结构。
|
||||
return {
|
||||
@ -259,17 +392,58 @@ export default {
|
||||
coverName: ''
|
||||
}
|
||||
},
|
||||
createEmptyEnglishForm() {
|
||||
return {
|
||||
title: '',
|
||||
summary: '',
|
||||
category: '',
|
||||
publishTime: '',
|
||||
coverUrl: '',
|
||||
coverName: '',
|
||||
content: ''
|
||||
}
|
||||
},
|
||||
handleOpen() {
|
||||
// 弹窗打开时合并默认值和待编辑文章数据。
|
||||
const article = this.article || {}
|
||||
const english = article.english || {}
|
||||
this.form = {
|
||||
...this.createEmptyForm(),
|
||||
...(this.article || {})
|
||||
...article,
|
||||
title: article.title || article.title_zh || '',
|
||||
summary: article.summary || article.summary_zh || '',
|
||||
type: article.type || article.article_type_zh || '企业动态',
|
||||
publishTime: article.publishTime || article.publish_time_zh || article.publish_time || '',
|
||||
content: article.content || article.content_zh || '',
|
||||
coverUrl: article.coverUrl || article.cover_img_zh || article.cover_img || '',
|
||||
coverName: article.coverName || article.cover_name_zh || article.cover_name || ''
|
||||
}
|
||||
// 没有发布时间时默认使用当天日期。
|
||||
if (!this.form.publishTime) this.form.publishTime = this.today
|
||||
// 初始化封面预览。
|
||||
this.coverPreview = this.form.coverUrl || ''
|
||||
this.editorFullscreen = false
|
||||
this.imageUploading = false
|
||||
this.englishImageUploading = false
|
||||
this.english = {
|
||||
...this.createEmptyEnglishForm(),
|
||||
...english,
|
||||
title: english.title || article.title_en || '',
|
||||
summary: english.summary || article.summary_en || '',
|
||||
category: english.category || article.article_type_en || '',
|
||||
publishTime: english.publishTime || article.publish_time_en || '',
|
||||
content: english.content || article.content_en || '',
|
||||
coverUrl: english.coverUrl || article.cover_img_en || '',
|
||||
coverName: english.coverName || article.cover_name_en || ''
|
||||
}
|
||||
this.showEnglishForm = Boolean(
|
||||
this.english.title ||
|
||||
this.english.summary ||
|
||||
this.english.category ||
|
||||
this.english.publishTime ||
|
||||
this.english.coverUrl ||
|
||||
this.english.content
|
||||
)
|
||||
this.$nextTick(() => {
|
||||
// 表单初始化完成后重置未保存状态。
|
||||
this.dirty = false
|
||||
@ -280,6 +454,12 @@ export default {
|
||||
// 保存编辑器实例,后续销毁时使用。
|
||||
this.editor = Object.seal(editor)
|
||||
},
|
||||
handleEnglishEditorCreated(editor) {
|
||||
this.englishEditor = Object.seal(editor)
|
||||
},
|
||||
handleEnglishEditorDestroyed() {
|
||||
this.englishEditor = null
|
||||
},
|
||||
handleDialogClose() {
|
||||
this.stopObserveEditorFullscreen()
|
||||
this.editorFullscreen = false
|
||||
@ -398,6 +578,23 @@ export default {
|
||||
event.target.value = ''
|
||||
}
|
||||
},
|
||||
async handleEnglishCoverUpload(event) {
|
||||
const file = event.target.files && event.target.files[0]
|
||||
if (!file) return
|
||||
|
||||
this.english.coverName = file.name
|
||||
this.englishImageUploading = true
|
||||
try {
|
||||
this.english.coverUrl = await this.uploadNewsImage(file)
|
||||
this.$message.success('英文封面图片上传成功')
|
||||
} catch (error) {
|
||||
this.english.coverName = ''
|
||||
this.$message.error('英文封面图片上传失败')
|
||||
} finally {
|
||||
this.englishImageUploading = false
|
||||
event.target.value = ''
|
||||
}
|
||||
},
|
||||
handleCoverUrlInput() {
|
||||
// 用户手动输入图片路径时,清空本地上传预览状态。
|
||||
this.coverPreview = ''
|
||||
@ -469,13 +666,16 @@ export default {
|
||||
this.$message.warning('请输入文章正文')
|
||||
return
|
||||
}
|
||||
if (this.imageUploading) {
|
||||
if (this.imageUploading || this.englishImageUploading) {
|
||||
this.$message.warning('图片上传中,请稍后保存')
|
||||
return
|
||||
}
|
||||
// 第六步:组装保存参数,status 由按钮决定是草稿还是发布。
|
||||
const payload = {
|
||||
...this.form,
|
||||
english: {
|
||||
...this.english
|
||||
},
|
||||
publish_time: this.form.publishTime,
|
||||
status
|
||||
}
|
||||
@ -505,13 +705,18 @@ export default {
|
||||
margin-right: 4px;
|
||||
}
|
||||
::v-deep .article-editor-dialog {
|
||||
--article-editor-radius: 8px;
|
||||
margin: 0 !important;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
padding:0 24px!important;
|
||||
padding-top: 16px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
::v-deep .article-editor-dialog .el-dialog__header {
|
||||
::v-deep .el-dialog__header {
|
||||
flex-shrink: 0;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
@ -535,7 +740,7 @@ export default {
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
border: 1px solid #d8e0ec;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--article-editor-radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
@ -554,11 +759,20 @@ export default {
|
||||
}
|
||||
|
||||
::v-deep .article-editor-dialog .el-dialog__footer {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
min-height: 60px;
|
||||
flex-shrink: 0;
|
||||
padding: 14px 24px;
|
||||
padding: 0 24px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
::v-deep .article-editor-dialog .el-button {
|
||||
border-radius: var(--article-editor-radius);
|
||||
}
|
||||
.article-editor {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@ -572,6 +786,21 @@ export default {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.article-editor__left.has-language-form {
|
||||
overflow-y: auto;
|
||||
scrollbar-color: #cbd5e1 transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.article-editor__left.has-language-form .article-editor__form {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.article-editor__left.has-language-form .wang-editor-wrap {
|
||||
flex: 0 0 420px;
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: relative;
|
||||
flex: 0 0 10px;
|
||||
@ -614,6 +843,70 @@ export default {
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.language-trigger {
|
||||
min-height: 48px;
|
||||
margin: 2px 0 12px 88px;
|
||||
padding: 9px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
background: linear-gradient(90deg, #f8fbff 0%, #f7f5ff 100%);
|
||||
border: 1px dashed #bfdbfe;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.language-trigger__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.language-trigger__badge {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #4f46e5;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
background: #ede9fe;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.language-trigger__title strong,
|
||||
.language-trigger__title small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.language-trigger__title strong {
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.language-trigger__title small {
|
||||
margin-top: 2px;
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.language-trigger__button {
|
||||
padding: 5px 8px;
|
||||
color: #4f46e5;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.language-trigger__button i {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.article-editor__inline {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
@ -671,6 +964,219 @@ export default {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.article-editor__language-section {
|
||||
flex-shrink: 0;
|
||||
margin-top: 22px;
|
||||
padding: 24px 18px 40px 0;
|
||||
border-top: 1px solid #dbeafe;
|
||||
}
|
||||
|
||||
.language-section-head {
|
||||
margin-bottom: 18px;
|
||||
padding: 16px 18px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
background: linear-gradient(135deg, #f5f9ff 0%, #f7f5ff 100%);
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.language-section-head__eyebrow {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #6366f1;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.language-section-head h3 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.language-section-head p {
|
||||
margin: 5px 0 0;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.language-section-head__tag {
|
||||
padding: 4px 9px;
|
||||
color: #4f46e5;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: #ede9fe;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.language-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.language-field--wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.language-field label {
|
||||
display: block;
|
||||
margin-bottom: 7px;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.language-field .el-select,
|
||||
.language-field .el-date-editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.language-cover {
|
||||
min-height: 32px;
|
||||
padding: 5px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
/* border: 1px solid #dcdfe6; */
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.language-cover__upload {
|
||||
padding: 5px 9px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
background: #409eff;
|
||||
border: 0;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.language-cover__upload i {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.language-cover__input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.language-cover__name {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
color: #16a34a;
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.language-richtext {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.language-richtext .wang-editor-wrap {
|
||||
height: 420px;
|
||||
min-height: 420px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.language-richtext .wang-toolbar {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.language-richtext__head {
|
||||
padding: 12px 14px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
border: 1px solid #edf0f5;
|
||||
border-bottom: 0;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
.language-richtext__head span {
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.language-richtext__head small {
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.language-richtext__toolbar {
|
||||
min-height: 38px;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #64748b;
|
||||
background: #f8fafc;
|
||||
border-top: 1px solid #edf2f7;
|
||||
border-bottom: 1px solid #edf2f7;
|
||||
}
|
||||
|
||||
.language-richtext__toolbar > span {
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
padding: 0 5px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.language-richtext__toolbar > span:not(.language-richtext__divider):hover {
|
||||
color: #2563eb;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.language-richtext__strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.language-richtext__italic {
|
||||
font-family: Georgia, serif;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.language-richtext__toolbar .language-richtext__divider {
|
||||
min-width: 1px;
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
margin: 0 4px;
|
||||
background: #dbe2ea;
|
||||
}
|
||||
|
||||
.language-richtext__canvas {
|
||||
min-height: 230px;
|
||||
padding: 16px;
|
||||
color: #c0c7d1;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
cursor: text;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.language-richtext__canvas:empty::before {
|
||||
content: attr(data-placeholder);
|
||||
color: #c0c7d1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.article-editor__preview {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
@ -757,13 +1263,31 @@ export default {
|
||||
}
|
||||
|
||||
.article-editor-footer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.editor-status {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.editor-footer-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
gap: 10px;
|
||||
|
||||
.el-button {
|
||||
min-height: 32px;
|
||||
margin-left: 0 !important;
|
||||
border-radius: var(--article-editor-radius);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -53,6 +53,16 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isEnglish() {
|
||||
return this.$i18n && this.$i18n.locale === 'en-US'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isEnglish() {
|
||||
this.getNewsDetail()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getNewsDetail()
|
||||
},
|
||||
@ -84,6 +94,10 @@ export default {
|
||||
if (/^(https?:|blob:|data:|\/\/)/.test(url)) return url
|
||||
return `${window.location.origin}/idfile?path=${url}`
|
||||
},
|
||||
getLocalizedField(row, zhField, enField, legacyField) {
|
||||
if (this.isEnglish && row[enField]) return row[enField]
|
||||
return row[zhField] || row[legacyField] || row[enField] || ''
|
||||
},
|
||||
getResponseDetail(res) {
|
||||
if (res.data && res.data.data && !Array.isArray(res.data.data)) return res.data.data
|
||||
if (res.data && !Array.isArray(res.data)) return res.data
|
||||
@ -94,12 +108,12 @@ export default {
|
||||
normalizeArticle(row) {
|
||||
return {
|
||||
id: row.id || this.$route.params.id,
|
||||
title: row.title || '未命名文章',
|
||||
summary: row.summary || '',
|
||||
articleType: row.article_type || row.type || '企业动态',
|
||||
publishTime: row.publish_time || row.publishTime || '',
|
||||
coverImg: this.normalizeImageUrl(row.cover_img || row.coverImg || ''),
|
||||
content: row.content || ''
|
||||
title: this.getLocalizedField(row, 'title_zh', 'title_en', 'title') || '未命名文章',
|
||||
summary: this.getLocalizedField(row, 'summary_zh', 'summary_en', 'summary'),
|
||||
articleType: this.getLocalizedField(row, 'article_type_zh', 'article_type_en', 'article_type') || row.type || '企业动态',
|
||||
publishTime: this.getLocalizedField(row, 'publish_time_zh', 'publish_time_en', 'publish_time') || row.publishTime || '',
|
||||
coverImg: this.normalizeImageUrl(this.getLocalizedField(row, 'cover_img_zh', 'cover_img_en', 'cover_img') || row.coverImg || ''),
|
||||
content: this.getLocalizedField(row, 'content_zh', 'content_en', 'content')
|
||||
}
|
||||
},
|
||||
async getNewsDetail() {
|
||||
|
||||
@ -190,7 +190,9 @@ export default {
|
||||
status: '',
|
||||
keyword: ''
|
||||
},
|
||||
articleList: []
|
||||
articleList: [],
|
||||
articleTypeSummary: [],
|
||||
articleTypeSummaryEn: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -215,12 +217,18 @@ export default {
|
||||
行业洞察: { label: '行业洞察', type: '行业洞察', icon: 'el-icon-data-analysis', iconClass: 'amber' },
|
||||
活动资讯: { label: '活动资讯', type: '活动资讯', icon: 'el-icon-date', iconClass: 'purple' }
|
||||
}
|
||||
const summaryMap = {}
|
||||
this.articleTypeSummary.forEach(item => {
|
||||
if (item && item.article_type) {
|
||||
summaryMap[item.article_type] = item
|
||||
}
|
||||
})
|
||||
return Object.values(map).map(item => {
|
||||
const list = this.articleList.filter(article => article.type === item.type)
|
||||
const summary = summaryMap[item.type] || {}
|
||||
return {
|
||||
...item,
|
||||
count: list.length,
|
||||
views: list.reduce((total, article) => total + Number(article.views || 0), 0)
|
||||
count: Number(summary.article_count || 0),
|
||||
views: Number(summary.read_count || 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -291,18 +299,39 @@ export default {
|
||||
// 将接口字段统一转换成页面表格和编辑弹窗使用的字段。
|
||||
return {
|
||||
id: row.id,
|
||||
title: row.title || '',
|
||||
summary: row.summary || '',
|
||||
type: row.article_type || row.type || '企业动态',
|
||||
title: row.title_zh || row.title || '',
|
||||
summary: row.summary_zh || row.summary || '',
|
||||
type: row.article_type_zh || row.article_type || row.type || '企业动态',
|
||||
status: this.getViewStatus(row.status),
|
||||
publishTime: row.publish_time || row.publishTime || '',
|
||||
publishTime: row.publish_time_zh || row.publish_time || row.publishTime || '',
|
||||
updateTime: row.update_time || row.updateTime || '',
|
||||
views: row.read_count || row.views || 0,
|
||||
content: row.content || '',
|
||||
coverUrl: row.cover_img || row.coverUrl || '',
|
||||
coverName: row.cover_name || row.coverName || ''
|
||||
content: row.content_zh || row.content || '',
|
||||
coverUrl: row.cover_img_zh || row.cover_img || row.coverUrl || '',
|
||||
coverName: row.cover_name_zh || row.cover_name || row.coverName || '',
|
||||
english: {
|
||||
title: row.title_en || '',
|
||||
summary: row.summary_en || '',
|
||||
category: row.article_type_en || '',
|
||||
publishTime: row.publish_time_en || '',
|
||||
content: row.content_en || '',
|
||||
coverUrl: row.cover_img_en || '',
|
||||
coverName: row.cover_name_en || ''
|
||||
}
|
||||
}
|
||||
},
|
||||
getArticleTypeSummary(res) {
|
||||
if (Array.isArray(res.article_type_summary)) return res.article_type_summary
|
||||
const data = res.data && !Array.isArray(res.data) ? res.data : {}
|
||||
if (Array.isArray(data.article_type_summary)) return data.article_type_summary
|
||||
return []
|
||||
},
|
||||
getArticleTypeSummaryEn(res) {
|
||||
if (Array.isArray(res.article_type_summary_en)) return res.article_type_summary_en
|
||||
const data = res.data && !Array.isArray(res.data) ? res.data : {}
|
||||
if (Array.isArray(data.article_type_summary_en)) return data.article_type_summary_en
|
||||
return []
|
||||
},
|
||||
getResponseList(res) {
|
||||
// 兼容接口可能返回的多种列表结构。
|
||||
if (Array.isArray(res.data)) return res.data
|
||||
@ -325,17 +354,24 @@ export default {
|
||||
)
|
||||
},
|
||||
buildArticlePayload(article) {
|
||||
// 将编辑弹窗字段转换成新增/编辑接口需要的字段。
|
||||
// 将中文、英文编辑字段转换成新增/编辑接口需要的双语字段。
|
||||
const english = article.english || {}
|
||||
return {
|
||||
...(article.id ? { id: article.id } : {}),
|
||||
url_link: window.location.href,
|
||||
title: article.title,
|
||||
article_type: article.type,
|
||||
summary: article.summary,
|
||||
title_zh: article.title,
|
||||
title_en: english.title || '',
|
||||
article_type_zh: article.type,
|
||||
article_type_en: english.category || '',
|
||||
summary_zh: article.summary,
|
||||
summary_en: english.summary || '',
|
||||
update_time: article.update_time || article.updateTime,
|
||||
publish_time: article.publish_time || article.publishTime,
|
||||
cover_img: article.coverUrl,
|
||||
content: article.content,
|
||||
publish_time_zh: article.publish_time_zh || article.publishTime,
|
||||
publish_time_en: english.publishTime || '',
|
||||
cover_img_zh: article.coverUrl,
|
||||
cover_img_en: english.coverUrl || '',
|
||||
content_zh: article.content,
|
||||
content_en: english.content || '',
|
||||
status: this.getApiStatus(article.status)
|
||||
}
|
||||
},
|
||||
@ -368,16 +404,19 @@ export default {
|
||||
const list = this.getResponseList(res)
|
||||
// 将接口数据映射成页面展示字段。
|
||||
this.articleList = list.map(item => this.normalizeArticle(item))
|
||||
// 读取接口总数,用于分页器和底部统计。
|
||||
this.articleTypeSummary = this.getArticleTypeSummary(res)
|
||||
this.articleTypeSummaryEn = this.getArticleTypeSummaryEn(res)
|
||||
this.total = this.getResponseTotal(res, list)
|
||||
return
|
||||
}
|
||||
// 接口返回失败时清空列表,避免展示旧数据。
|
||||
this.articleList = []
|
||||
this.articleTypeSummary = []
|
||||
this.articleTypeSummaryEn = []
|
||||
this.total = 0
|
||||
} catch (error) {
|
||||
// 请求异常时清空列表并提示用户。
|
||||
this.articleList = []
|
||||
this.articleTypeSummary = []
|
||||
this.articleTypeSummaryEn = []
|
||||
this.total = 0
|
||||
this.$message.error('文章列表加载失败')
|
||||
} finally {
|
||||
@ -726,4 +765,4 @@ export default {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -9,12 +9,11 @@
|
||||
<div class="news-hero-glow news-hero-glow-2"></div>
|
||||
<div class="news-shell">
|
||||
<div class="hero-content">
|
||||
<span class="hero-badge">COMPANY NEWS</span>
|
||||
<span class="hero-badge">{{ $t('newsView.badge') }}</span>
|
||||
<div class="hero-title-row">
|
||||
<h1>企业动态</h1>
|
||||
<button type="button" class="about-link" @click="goAbout">关于我们 →</button>
|
||||
<h1>{{ $t('newsView.title') }}</h1>
|
||||
</div>
|
||||
<p>了解开元云最新动态,把握AI行业前沿资讯,与我们一起见证智能跃迁</p>
|
||||
<p>{{ $t('newsView.intro') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -23,7 +22,7 @@
|
||||
<div class="news-shell">
|
||||
<div class="filter-tabs">
|
||||
<button
|
||||
v-for="item in filterTabs"
|
||||
v-for="item in localizedFilterTabs"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
class="filter-tab"
|
||||
@ -40,7 +39,7 @@
|
||||
<div class="news-shell">
|
||||
<article v-if="featuredNews" class="news-featured" @click="goNewsDetail(featuredNews)">
|
||||
<div class="news-item-glow"></div>
|
||||
<span class="hover-detail">详情 <i class="el-icon-arrow-right"></i></span>
|
||||
<span class="hover-detail">{{ $t('newsView.detail') }} <i class="el-icon-arrow-right"></i></span>
|
||||
<div class="news-featured-inner">
|
||||
<div class="news-featured-img">
|
||||
<img :src="featuredNews.img || fallbackNewsImage" :alt="featuredNews.title" @error="handleImageError">
|
||||
@ -69,7 +68,7 @@
|
||||
@click="goNewsDetail(item)"
|
||||
>
|
||||
<div class="news-item-glow"></div>
|
||||
<span class="hover-detail">详情 <i class="el-icon-arrow-right"></i></span>
|
||||
<span class="hover-detail">{{ $t('newsView.detail') }} <i class="el-icon-arrow-right"></i></span>
|
||||
<div class="news-item-img">
|
||||
<img :src="item.img || fallbackNewsImage" :alt="item.title" @error="handleImageError">
|
||||
<div class="news-item-img-overlay"></div>
|
||||
@ -101,7 +100,7 @@
|
||||
<i class="el-icon-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@ -115,26 +114,34 @@ export default {
|
||||
fallbackNewsImage: require('@/assets/image/news.jpg'),
|
||||
activeCategory: 'all',
|
||||
page: 1,
|
||||
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
filterTabs: [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '企业动态', value: '企业动态' },
|
||||
{ label: '产品动态', value: '产品动态' },
|
||||
{ label: '行业洞察', value: '行业洞察' },
|
||||
{ label: '活动资讯', value: '活动资讯' }
|
||||
filterTabValues: [
|
||||
{ key: 'all', value: 'all', zh: '', en: '' },
|
||||
{ key: 'corporate', value: 'corporate', zh: '企业动态', en: 'Corporate News' },
|
||||
{ key: 'product', value: 'product', zh: '产品动态', en: 'Product News' },
|
||||
{ key: 'industry', value: 'industry', zh: '行业洞察', en: 'Industry Insights' },
|
||||
{ key: 'event', value: 'event', zh: '活动资讯', en: 'Event News' }
|
||||
],
|
||||
newsList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isEnglish() {
|
||||
return this.$i18n && this.$i18n.locale === 'en-US'
|
||||
},
|
||||
featuredNews() {
|
||||
return this.newsList[0]
|
||||
},
|
||||
filteredNewsList() {
|
||||
return this.newsList.slice(1)
|
||||
},
|
||||
localizedFilterTabs() {
|
||||
return this.filterTabValues.map(item => ({
|
||||
...item,
|
||||
label: item.key === 'all' ? this.$t('newsView.all') : this.$t(`newsView.categories.${item.key}`)
|
||||
}))
|
||||
},
|
||||
totalPages() {
|
||||
return Math.max(Math.ceil(this.total / this.pageSize), 1)
|
||||
},
|
||||
@ -142,6 +149,11 @@ export default {
|
||||
return Array.from({ length: Math.min(this.totalPages, 3) }, (_, index) => index + 1)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isEnglish() {
|
||||
this.getNewsList()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getNewsList()
|
||||
},
|
||||
@ -173,23 +185,57 @@ export default {
|
||||
this.page = page
|
||||
this.getNewsList()
|
||||
},
|
||||
getActiveCategoryConfig() {
|
||||
return this.filterTabValues.find(item => item.value === this.activeCategory) || this.filterTabValues[0]
|
||||
},
|
||||
buildNewsParams() {
|
||||
const category = this.getActiveCategoryConfig()
|
||||
return {
|
||||
url_link: window.location.href.split('#')[0] || window.location.href,
|
||||
title_zh: '',
|
||||
title_en: '',
|
||||
article_type_zh: category.zh || '',
|
||||
article_type_en: category.en || '',
|
||||
current_page: String(this.page),
|
||||
page_size: String(this.pageSize),
|
||||
article_type: this.activeCategory === 'all' ? '' : this.activeCategory,
|
||||
title: ''
|
||||
page_size: String(this.pageSize)
|
||||
}
|
||||
},
|
||||
getTagColor(type) {
|
||||
const map = {
|
||||
企业动态: 'rgba(16,185,129,0.85)',
|
||||
产品动态: 'rgba(99,102,241,0.85)',
|
||||
行业洞察: 'rgba(13,148,136,0.85)',
|
||||
活动资讯: 'rgba(245,158,11,0.85)'
|
||||
corporate: 'rgba(16,185,129,0.85)',
|
||||
product: 'rgba(99,102,241,0.85)',
|
||||
industry: 'rgba(13,148,136,0.85)',
|
||||
event: 'rgba(245,158,11,0.85)'
|
||||
}
|
||||
return map[type] || 'rgba(99,102,241,0.85)'
|
||||
return map[this.getCategoryKey(type)] || 'rgba(99,102,241,0.85)'
|
||||
},
|
||||
getCategoryKey(type) {
|
||||
const map = {
|
||||
企业动态: 'corporate',
|
||||
'Corporate News': 'corporate',
|
||||
产品动态: 'product',
|
||||
'Product News': 'product',
|
||||
行业洞察: 'industry',
|
||||
'Industry Insights': 'industry',
|
||||
活动资讯: 'event',
|
||||
'Event News': 'event'
|
||||
}
|
||||
if (map[type]) return map[type]
|
||||
const matchedLabel = Object.keys(map).find(label => String(type || '').startsWith(label))
|
||||
return matchedLabel ? map[matchedLabel] : ''
|
||||
},
|
||||
getCategorySuffix(type) {
|
||||
const labels = ['企业动态', 'Corporate News', '产品动态', 'Product News', '行业洞察', 'Industry Insights', '活动资讯', 'Event News']
|
||||
const matchedLabel = labels.find(label => String(type || '').startsWith(label))
|
||||
return matchedLabel ? String(type).slice(matchedLabel.length) : ''
|
||||
},
|
||||
getLocalizedCategory(type) {
|
||||
const key = this.getCategoryKey(type)
|
||||
return key ? `${this.$t(`newsView.categories.${key}`)}${this.getCategorySuffix(type)}` : type
|
||||
},
|
||||
getLocalizedField(row, zhField, enField, legacyField) {
|
||||
if (this.isEnglish && row[enField]) return row[enField]
|
||||
return row[zhField] || row[legacyField] || row[enField] || ''
|
||||
},
|
||||
normalizeImageUrl(url) {
|
||||
if (!url) return ''
|
||||
@ -197,16 +243,17 @@ export default {
|
||||
return `${window.location.origin}/idfile?path=${url}`
|
||||
},
|
||||
normalizeNewsItem(row) {
|
||||
const type = row.article_type || '企业动态'
|
||||
const type = this.getLocalizedField(row, 'article_type_zh', 'article_type_en', 'article_type') || '企业动态'
|
||||
const localizedType = this.getLocalizedCategory(type)
|
||||
return {
|
||||
id: row.id,
|
||||
category: type,
|
||||
tag: type,
|
||||
category: localizedType,
|
||||
tag: localizedType,
|
||||
tagColor: this.getTagColor(type),
|
||||
date: row.publish_time || '',
|
||||
title: row.title || '未命名文章',
|
||||
desc: row.summary || '',
|
||||
img: this.normalizeImageUrl(row.cover_img || ''),
|
||||
date: this.getLocalizedField(row, 'publish_time_zh', 'publish_time_en', 'publish_time'),
|
||||
title: this.getLocalizedField(row, 'title_zh', 'title_en', 'title') || '未命名文章',
|
||||
desc: this.getLocalizedField(row, 'summary_zh', 'summary_en', 'summary'),
|
||||
img: this.normalizeImageUrl(this.getLocalizedField(row, 'cover_img_zh', 'cover_img_en', 'cover_img')),
|
||||
views: row.read_count || 0
|
||||
}
|
||||
},
|
||||
@ -226,7 +273,7 @@ export default {
|
||||
} catch (error) {
|
||||
this.newsList = []
|
||||
this.total = 0
|
||||
this.$message.error('新闻列表加载失败')
|
||||
this.$message.error(this.$t('newsView.loadFail'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,6 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
custom-class="forgot-password-dialog"
|
||||
:visible="visible"
|
||||
width="540px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
@open="handleOpen"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-dialog custom-class="forgot-password-dialog" :visible="visible" width="540px" :close-on-click-modal="false"
|
||||
destroy-on-close append-to-body @open="handleOpen" @close="handleClose">
|
||||
<div slot="title" class="forgot-dialog-title">
|
||||
<div class="title-icon">
|
||||
<i class="el-icon-lock"></i>
|
||||
@ -25,19 +17,16 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="新密码" prop="password">
|
||||
<el-input v-model="form.password" clearable show-password autocomplete="new-password" placeholder="请输入新密码"></el-input>
|
||||
<el-input v-model="form.password" clearable show-password autocomplete="new-password"
|
||||
placeholder="请输入新密码"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="验证码" prop="vcode">
|
||||
<div class="code-row">
|
||||
<el-input v-model="form.vcode" clearable autocomplete="off" placeholder="请输入验证码"></el-input>
|
||||
<el-button
|
||||
class="code-btn"
|
||||
:disabled="isDisabled || isGettingCode"
|
||||
:loading="isGettingCode"
|
||||
@click="debouncedGetCode"
|
||||
>
|
||||
{{ sendCodeText }}
|
||||
<el-button class="code-btn" :disabled="isDisabled || isGettingCode" :loading="isGettingCode"
|
||||
@click="debouncedGetCode">
|
||||
{{ sendCodeText }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@ -288,18 +277,21 @@ export default {
|
||||
}
|
||||
|
||||
.code-btn {
|
||||
width: 118px;
|
||||
height: 48px;
|
||||
color: #2f6bff;
|
||||
background: #eef4ff;
|
||||
border-color: #cfe0ff;
|
||||
padding: 0 18px;
|
||||
color: #1d4ed8;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(37, 99, 235, 0.35);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: #ffffff;
|
||||
background: #2f6bff;
|
||||
border-color: #2f6bff;
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,9 +262,10 @@ export default {
|
||||
logoInfoNew: state => state.product.logoInfoNew, // Logo信息
|
||||
}),
|
||||
|
||||
// 检查当前域名是否为ncmatch.cn
|
||||
// 检查当前域名是否为 NCMatch 系列官网。
|
||||
isNcmatchDomain() {
|
||||
return window.location.hostname.includes('ncmatch.cn');
|
||||
const hostname = window.location.hostname
|
||||
return hostname.includes('ncmatch.cn') || hostname.includes('zgcopc.opencomputing.cn');
|
||||
},
|
||||
activeRules() {
|
||||
if (this.loginMode === 'mobile') {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="product-service-page" :class="{ 'single-product-page': isSinglePageMode }">
|
||||
|
||||
|
||||
<!-- 产品分类导航 -->
|
||||
<div v-if="!isSinglePageMode" class="category-nav">
|
||||
<div v-for="category in panelData"
|
||||
@ -118,12 +118,12 @@
|
||||
<div class="token-card-top">
|
||||
<span class="token-provider-avatar">
|
||||
<img
|
||||
v-if="product.model_logo && !product.logoLoadFailed"
|
||||
v-if="getModelLogoUrl(product.model_logo) && !product.logoLoadFailed"
|
||||
:src="getModelLogoUrl(product.model_logo)"
|
||||
alt=""
|
||||
:alt="product.display_name || product.model_name || '模型logo'"
|
||||
@error="handleModelLogoError(product)"
|
||||
>
|
||||
<span v-else>{{ getProviderInitial(product.provider || product.display_name || product.model_name) }}</span>
|
||||
<template v-else>{{ getProviderInitial(product.provider || product.display_name) }}</template>
|
||||
</span>
|
||||
|
||||
<div class="token-title-group">
|
||||
@ -566,7 +566,8 @@ export default {
|
||||
return window.location.href;
|
||||
}
|
||||
const baseUrl = window.location.href.split('#')[0];
|
||||
const homePath = window.location.hostname.includes('ncmatch.cn') ? '/ncmatchHome/index' : '/homePage/index';
|
||||
const hostname = window.location.hostname
|
||||
const homePath = hostname.includes('ncmatch.cn') || hostname.includes('zgcopc.opencomputing.cn') ? '/ncmatchHome/index' : '/homePage/index';
|
||||
return `${baseUrl}#${homePath}`;
|
||||
},
|
||||
|
||||
@ -1594,16 +1595,16 @@ export default {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
// background: #7c3aed;
|
||||
// border-radius: 8px;
|
||||
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 50%, #2563eb 100%);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
// box-shadow: 0 6px 16px rgba(124, 58, 237, 0.18);
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,18 +14,19 @@ export function getHomePath() {
|
||||
|
||||
// 如果是业主机构
|
||||
if ((domain_url.includes('ncmatch') ||
|
||||
domain_url.includes('zgcopc') ||
|
||||
domain_url.includes('9527') ||
|
||||
domain_url.includes('8889') ||
|
||||
domain_url.includes('8891') ||
|
||||
['xterm.kaiyuancloud.cn', 'www.kaiyuancloud.cn', 'dev.kaiyuancloud.cn', 'dev.opencomputing.cn', 'test.kaiyuancloud.cn', 'localhost'].includes(domain_url)) &&
|
||||
!url_link.includes('/domain/')) {
|
||||
|
||||
if (domain_url.includes('ncmatch') || domain_url.includes('9527')) {
|
||||
if (domain_url.includes('ncmatch') || domain_url.includes('zgcopc') || domain_url.includes('9527')) {
|
||||
homePath = '/ncmatchHome/index'
|
||||
} else if (domain_url.includes('kaiyuancloud') || domain_url.includes('opencomputing') || domain_url.includes('localhost')) {
|
||||
homePath = '/homePage/index'
|
||||
}
|
||||
} else if (hostname.includes('ncmatch.cn')) {
|
||||
} else if (hostname.includes('ncmatch.cn') || hostname.includes('zgcopc.opencomputing.cn')) {
|
||||
homePath = '/ncmatchHome/index'
|
||||
} else {
|
||||
homePath = '/homePage/index'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user