main #138
73
b/docs/upload_file.dspy
Normal file
73
b/docs/upload_file.dspy
Normal file
@ -0,0 +1,73 @@
|
||||
async def upload_file(ns={}):
|
||||
import os
|
||||
import base64
|
||||
import datetime
|
||||
|
||||
file_name = ns.get('file_name')
|
||||
file_content = ns.get('file_content') or ns.get('file_base64')
|
||||
storage_type = ns.get('storage_type', 'local')
|
||||
|
||||
if not file_name:
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '请传递file_name'
|
||||
}
|
||||
if not file_content:
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '请传递file_content'
|
||||
}
|
||||
if storage_type != 'local':
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '暂不支持该存储类型'
|
||||
}
|
||||
|
||||
safe_file_name = os.path.basename(file_name).replace('\\', '').replace('/', '')
|
||||
if not safe_file_name:
|
||||
return {
|
||||
'status': False,
|
||||
'msg': 'file_name错误'
|
||||
}
|
||||
|
||||
try:
|
||||
if ',' in file_content and file_content.split(',', 1)[0].startswith('data:'):
|
||||
file_content = file_content.split(',', 1)[1]
|
||||
file_bytes = base64.b64decode(file_content)
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '文件内容解析失败, %s' % str(e)
|
||||
}
|
||||
|
||||
now_date = datetime.datetime.now()
|
||||
date_path = now_date.strftime('%Y/%m/%d')
|
||||
relative_path = '%s/%s' % (date_path, safe_file_name)
|
||||
base_path = '/data'
|
||||
save_dir = os.path.join(base_path, now_date.strftime('%Y'), now_date.strftime('%m'), now_date.strftime('%d'))
|
||||
save_path = os.path.join(save_dir, safe_file_name)
|
||||
|
||||
try:
|
||||
if not os.path.exists(save_dir):
|
||||
os.makedirs(save_dir)
|
||||
with open(save_path, 'wb') as f:
|
||||
f.write(file_bytes)
|
||||
return {
|
||||
'status': True,
|
||||
'msg': 'upload success',
|
||||
'data': {
|
||||
'storage_type': storage_type,
|
||||
'file_name': safe_file_name,
|
||||
'file_path': relative_path,
|
||||
'save_path': save_path,
|
||||
'file_size': len(file_bytes)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '文件保存失败, %s' % str(e)
|
||||
}
|
||||
|
||||
ret = await upload_file(params_kw)
|
||||
return ret
|
||||
16
b/news/enterprise_news_article.sql
Normal file
16
b/news/enterprise_news_article.sql
Normal file
@ -0,0 +1,16 @@
|
||||
CREATE TABLE `enterprise_news_article` (
|
||||
`id` varchar(32) NOT NULL COMMENT '唯一标识符',
|
||||
`domain_name` varchar(64) NOT NULL COMMENT '所属域名',
|
||||
`title` varchar(100) NOT NULL COMMENT '文章标题',
|
||||
`article_type` varchar(20) NOT NULL COMMENT '文章类型(企业动态/产品动态/行业洞察/活动资讯)',
|
||||
`summary` varchar(255) DEFAULT NULL COMMENT '文章摘要',
|
||||
`cover_img` varchar(255) DEFAULT NULL COMMENT '封面图片',
|
||||
`content` text 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='企业文章表';
|
||||
37
b/news/front_news_detail.dspy
Normal file
37
b/news/front_news_detail.dspy
Normal file
@ -0,0 +1,37 @@
|
||||
async def front_news_detail(ns={}):
|
||||
if not ns.get('id'):
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '请传递id'
|
||||
}
|
||||
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
try:
|
||||
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
|
||||
from enterprise_news_article
|
||||
where id = '%s' and status = '1' and del_flg = '0';
|
||||
""" % ns.get('id')
|
||||
result = await sor.sqlExe(search_sql, {})
|
||||
if not result:
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '文章不存在或未发布'
|
||||
}
|
||||
return {
|
||||
'status': True,
|
||||
'msg': 'search front news detail success',
|
||||
'data': result[0]
|
||||
}
|
||||
except Exception as e:
|
||||
await sor.rollback()
|
||||
return {
|
||||
'status': False,
|
||||
'msg': 'search front news detail failed, %s' % str(e)
|
||||
}
|
||||
|
||||
ret = await front_news_detail(params_kw)
|
||||
return ret
|
||||
52
b/news/front_news_search.dspy
Normal file
52
b/news/front_news_search.dspy
Normal file
@ -0,0 +1,52 @@
|
||||
async def front_news_search(ns={}):
|
||||
if not ns.get('url_link'):
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '请传递url_link'
|
||||
}
|
||||
domain_name = ns.get('url_link').split("//")[1].split("/")[0]
|
||||
if 'localhost' in domain_name:
|
||||
domain_name = 'dev.opencomputing.cn'
|
||||
|
||||
current_page = int(ns.get('current_page', 1))
|
||||
page_size = int(ns.get('page_size', 10))
|
||||
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'))
|
||||
where_clause = " and ".join(conditions)
|
||||
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
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']
|
||||
search_sql = """
|
||||
select id, title, article_type, summary, cover_img, publish_time, read_count
|
||||
from enterprise_news_article
|
||||
where %s
|
||||
order by publish_time desc, update_time desc
|
||||
limit %s offset %s;
|
||||
""" % (where_clause, page_size, offset)
|
||||
result = await sor.sqlExe(search_sql, {})
|
||||
return {
|
||||
'status': True,
|
||||
'msg': 'search front news success',
|
||||
'data': result,
|
||||
'pagination': {
|
||||
'total': total_count,
|
||||
'page_size': page_size,
|
||||
'current_page': current_page
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': False,
|
||||
'msg': 'search front news failed, %s' % str(e)
|
||||
}
|
||||
|
||||
ret = await front_news_search(params_kw)
|
||||
return ret
|
||||
62
b/news/news_article_add.dspy
Normal file
62
b/news/news_article_add.dspy
Normal file
@ -0,0 +1,62 @@
|
||||
async def news_article_add(ns={}):
|
||||
if not ns.get('url_link'):
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '请传递url_link'
|
||||
}
|
||||
if not ns.get('title'):
|
||||
return {
|
||||
'status': False,
|
||||
'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:
|
||||
domain_name = 'dev.opencomputing.cn'
|
||||
|
||||
status = ns.get('status', '0')
|
||||
if status not in ['0', '1']:
|
||||
status = '0'
|
||||
|
||||
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'),
|
||||
'status': status,
|
||||
'publish_time': ns.get('publish_time'),
|
||||
'read_count': 0,
|
||||
'del_flg': '0'
|
||||
}
|
||||
|
||||
db = DBPools()
|
||||
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')
|
||||
await sor.sqlExe(publish_sql, {})
|
||||
return {
|
||||
'status': True,
|
||||
'msg': 'create news article success',
|
||||
'data': {
|
||||
'id': ns_dic.get('id')
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
await sor.rollback()
|
||||
return {
|
||||
'status': False,
|
||||
'msg': 'create news article failed, %s' % str(e)
|
||||
}
|
||||
|
||||
ret = await news_article_add(params_kw)
|
||||
return ret
|
||||
28
b/news/news_article_delete.dspy
Normal file
28
b/news/news_article_delete.dspy
Normal file
@ -0,0 +1,28 @@
|
||||
async def news_article_delete(ns={}):
|
||||
if not ns.get('id'):
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '请传递id'
|
||||
}
|
||||
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
try:
|
||||
ns_dic = {
|
||||
'id': ns.get('id'),
|
||||
'del_flg': '1'
|
||||
}
|
||||
await sor.U('enterprise_news_article', ns_dic)
|
||||
return {
|
||||
'status': True,
|
||||
'msg': 'delete news article success'
|
||||
}
|
||||
except Exception as e:
|
||||
await sor.rollback()
|
||||
return {
|
||||
'status': False,
|
||||
'msg': 'delete news article failed, %s' % str(e)
|
||||
}
|
||||
|
||||
ret = await news_article_delete(params_kw)
|
||||
return ret
|
||||
31
b/news/news_article_detail.dspy
Normal file
31
b/news/news_article_detail.dspy
Normal file
@ -0,0 +1,31 @@
|
||||
async def news_article_detail(ns={}):
|
||||
if not ns.get('id'):
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '请传递id'
|
||||
}
|
||||
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
try:
|
||||
search_sql = """select * from enterprise_news_article where id = '%s' and del_flg = '0';""" % ns.get('id')
|
||||
result = await sor.sqlExe(search_sql, {})
|
||||
if not result:
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '文章不存在'
|
||||
}
|
||||
result[0]['status_name'] = {'0': '草稿', '1': '已发布'}.get(str(result[0].get('status')), result[0].get('status'))
|
||||
return {
|
||||
'status': True,
|
||||
'msg': 'search news article detail success',
|
||||
'data': result[0]
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': False,
|
||||
'msg': 'search news article detail failed, %s' % str(e)
|
||||
}
|
||||
|
||||
ret = await news_article_detail(params_kw)
|
||||
return ret
|
||||
28
b/news/news_article_offline.dspy
Normal file
28
b/news/news_article_offline.dspy
Normal file
@ -0,0 +1,28 @@
|
||||
async def news_article_offline(ns={}):
|
||||
if not ns.get('id'):
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '请传递id'
|
||||
}
|
||||
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
try:
|
||||
ns_dic = {
|
||||
'id': ns.get('id'),
|
||||
'status': '0'
|
||||
}
|
||||
await sor.U('enterprise_news_article', ns_dic)
|
||||
return {
|
||||
'status': True,
|
||||
'msg': 'offline news article success'
|
||||
}
|
||||
except Exception as e:
|
||||
await sor.rollback()
|
||||
return {
|
||||
'status': False,
|
||||
'msg': 'offline news article failed, %s' % str(e)
|
||||
}
|
||||
|
||||
ret = await news_article_offline(params_kw)
|
||||
return ret
|
||||
29
b/news/news_article_publish.dspy
Normal file
29
b/news/news_article_publish.dspy
Normal file
@ -0,0 +1,29 @@
|
||||
async def news_article_publish(ns={}):
|
||||
if not ns.get('id'):
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '请传递id'
|
||||
}
|
||||
|
||||
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')
|
||||
await sor.sqlExe(publish_sql, {})
|
||||
return {
|
||||
'status': True,
|
||||
'msg': 'publish news article success'
|
||||
}
|
||||
except Exception as e:
|
||||
await sor.rollback()
|
||||
return {
|
||||
'status': False,
|
||||
'msg': 'publish news article failed, %s' % str(e)
|
||||
}
|
||||
|
||||
ret = await news_article_publish(params_kw)
|
||||
return ret
|
||||
78
b/news/news_article_search.dspy
Normal file
78
b/news/news_article_search.dspy
Normal file
@ -0,0 +1,78 @@
|
||||
async def news_article_search(ns={}):
|
||||
if not ns.get('url_link'):
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '请传递url_link'
|
||||
}
|
||||
domain_name = ns.get('url_link').split("//")[1].split("/")[0]
|
||||
if 'localhost' in domain_name:
|
||||
domain_name = 'dev.opencomputing.cn'
|
||||
|
||||
current_page = int(ns.get('current_page', 1))
|
||||
page_size = int(ns.get('page_size', 10))
|
||||
offset = (current_page - 1) * page_size
|
||||
|
||||
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('status'):
|
||||
conditions.append("status = '%s'" % ns.get('status'))
|
||||
where_clause = " and ".join(conditions)
|
||||
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('kboss') as sor:
|
||||
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
|
||||
from enterprise_news_article
|
||||
where %s
|
||||
group by article_type;
|
||||
""" % " 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 = []
|
||||
for article_type in ['企业动态', '产品动态', '行业洞察', '活动资讯']:
|
||||
summary_dic = summary_mapping.get(article_type, {})
|
||||
article_type_summary.append({
|
||||
'article_type': 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
|
||||
from enterprise_news_article
|
||||
where %s
|
||||
order by publish_time desc, update_time desc
|
||||
limit %s offset %s;
|
||||
""" % (where_clause, page_size, offset)
|
||||
result = await sor.sqlExe(search_sql, {})
|
||||
for data_dic in result:
|
||||
data_dic['status_name'] = {'0': '草稿', '1': '已发布'}.get(str(data_dic.get('status')), data_dic.get('status'))
|
||||
data_dic['operation_list'] = ['发布', '删除'] if str(data_dic.get('status')) == '0' else ['下架', '删除']
|
||||
return {
|
||||
'status': True,
|
||||
'msg': 'search news article success',
|
||||
'data': result,
|
||||
'article_type_summary': article_type_summary,
|
||||
'pagination': {
|
||||
'total': total_count,
|
||||
'page_size': page_size,
|
||||
'current_page': current_page
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': False,
|
||||
'msg': 'search news article failed, %s' % str(e)
|
||||
}
|
||||
|
||||
ret = await news_article_search(params_kw)
|
||||
return ret
|
||||
55
b/news/news_article_update.dspy
Normal file
55
b/news/news_article_update.dspy
Normal file
@ -0,0 +1,55 @@
|
||||
async def news_article_update(ns={}):
|
||||
if not ns.get('id'):
|
||||
return {
|
||||
'status': False,
|
||||
'msg': '请传递id'
|
||||
}
|
||||
|
||||
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')
|
||||
if 'status' in ns:
|
||||
if ns.get('status') not in ['0', '1']:
|
||||
return {
|
||||
'status': False,
|
||||
'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')
|
||||
await sor.sqlExe(publish_sql, {})
|
||||
return {
|
||||
'status': True,
|
||||
'msg': 'update news article success'
|
||||
}
|
||||
except Exception as e:
|
||||
await sor.rollback()
|
||||
return {
|
||||
'status': False,
|
||||
'msg': 'update news article failed, %s' % str(e)
|
||||
}
|
||||
|
||||
ret = await news_article_update(params_kw)
|
||||
return ret
|
||||
@ -22,6 +22,8 @@
|
||||
"dependencies": {
|
||||
"@form-create/element-ui": "^2.5.30",
|
||||
"@jiaminghi/data-view": "^2.10.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^1.0.2",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"amfe-flexible": "^2.2.1",
|
||||
"axios": "0.18.1",
|
||||
|
||||
74
f/web-kboss/src/api/newsapi/newsapi.js
Normal file
74
f/web-kboss/src/api/newsapi/newsapi.js
Normal file
@ -0,0 +1,74 @@
|
||||
import request from "@/utils/request";
|
||||
// 前台新闻列表
|
||||
export const reqNewsList = (data) => {
|
||||
return request({
|
||||
url: '/news/front_news_search.dspy',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
// 后台新闻列表
|
||||
export const reqNewsListAdmin = (data) => {
|
||||
return request({
|
||||
url: '/news/news_article_search.dspy',
|
||||
method: 'get',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
// 新闻详情
|
||||
export const reqNewsDetail = (data) => {
|
||||
// 调用文章详情接口,用于编辑时获取完整回显数据。
|
||||
return request({
|
||||
// 详情接口地址。
|
||||
url: '/news/front_news_detail.dspy',
|
||||
// 详情接口使用 GET 查询。
|
||||
method: 'get',
|
||||
// 传入文章 id 和当前页面 url_link 等查询参数。
|
||||
params: data
|
||||
})
|
||||
}
|
||||
// 添加新闻
|
||||
export const reqAddNews = (data) => {
|
||||
const isFormData = data instanceof FormData
|
||||
return request({
|
||||
url: '/news/news_article_add.dspy',
|
||||
method: isFormData ? 'post' : 'get',
|
||||
params: isFormData ? undefined : data,
|
||||
data: isFormData ? data : undefined,
|
||||
headers: isFormData ? { 'Content-Type': 'multipart/form-data' } : undefined
|
||||
})
|
||||
}
|
||||
// 编辑新闻
|
||||
export const reqEditNews = (data) => {
|
||||
const isFormData = data instanceof FormData
|
||||
return request({
|
||||
url: '/news/news_article_update.dspy',
|
||||
method: 'post',
|
||||
data,
|
||||
headers: isFormData ? { 'Content-Type': 'multipart/form-data' } : undefined
|
||||
})
|
||||
}
|
||||
// 删除新闻
|
||||
export const reqDeleteNews = (data) => {
|
||||
return request({
|
||||
url: '/news/news_article_delete.dspy',
|
||||
method: 'get',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
// 上架新闻
|
||||
export const reqPublishNews = (data) => {
|
||||
return request({
|
||||
url: '/news/news_article_publish.dspy',
|
||||
method: 'get',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
// 下架新闻
|
||||
export const reqUnpublishNews = (data) => {
|
||||
return request({
|
||||
url: '/news/news_article_offline.dspy',
|
||||
method: 'get',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
@ -118,6 +118,7 @@ export default {
|
||||
.happy-scroll-content {
|
||||
width: 100%;
|
||||
min-width: unset !important;
|
||||
padding-bottom: 78px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
@ -159,6 +160,7 @@ export default {
|
||||
height: 100%;
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
padding-bottom: 78px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.el-submenu__title,
|
||||
|
||||
@ -598,6 +598,32 @@ export const asyncRoutes = [
|
||||
]
|
||||
},
|
||||
|
||||
// 运营——新闻动态
|
||||
{
|
||||
path: "/newsLog",
|
||||
component: Layout,
|
||||
meta: {
|
||||
title: "新闻动态",
|
||||
fullPath: "/newsLog",
|
||||
noCache: true,
|
||||
icon: "el-icon-document",
|
||||
roles: ["运营"]
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: () => import('@/views/homePage/news/newsLog.vue'),
|
||||
name: 'NewsLog',
|
||||
meta: {
|
||||
title: "新闻动态",
|
||||
fullPath: "/newsLog",
|
||||
noCache: true,
|
||||
roles: ["运营"]
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
// token市集 - 一级菜单(所有登录用户都能看到)
|
||||
{
|
||||
|
||||
@ -18,7 +18,7 @@ const SUPER_ADMIN_ROUTE_PATH = '/superAdministrator';
|
||||
const COMMON_ROUTE_PATHS = ['/product', '/tokenManagement', '/tokenUsage', '/modelExperience', '/modelDetail', '/modelApiDocument'];
|
||||
|
||||
// 运营角色需要额外补出来的菜单。
|
||||
const OPERATION_EXTRA_ROUTE_PATHS = ['/modelManagement', '/modelInfoConfig', '/operationReport'];
|
||||
const OPERATION_EXTRA_ROUTE_PATHS = ['/modelManagement', '/modelInfoConfig', '/operationReport', '/newsLog'];
|
||||
|
||||
// 财务角色需要额外补出来的菜单。
|
||||
const FINANCE_EXTRA_ROUTE_PATHS = ['/financialOverview'];
|
||||
|
||||
810
f/web-kboss/src/views/LoginDialog/LoginDialog.vue
Normal file
810
f/web-kboss/src/views/LoginDialog/LoginDialog.vue
Normal file
@ -0,0 +1,810 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog
|
||||
:visible.sync="dialogVisible"
|
||||
width="540px"
|
||||
append-to-body
|
||||
custom-class="glass-dialog"
|
||||
:close-on-click-modal="true"
|
||||
:close-on-press-escape="true"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="login-card">
|
||||
<div class="brand-area">
|
||||
|
||||
<div class="brand-text">
|
||||
<h2>{{ activeTab === 'login' ? '欢迎回来' : '创建账号' }}</h2>
|
||||
<p>{{ activeTab === 'login' ? '登录后继续使用智能服务' : '注册开启全新体验' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-group">
|
||||
<button type="button" :class="{ active: activeTab === 'login' }" @click="switchTab('login')">登录</button>
|
||||
<button type="button" :class="{ active: activeTab === 'register' }" @click="switchTab('register')">注册</button>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'login'" class="form-panel">
|
||||
<div class="mode-tabs">
|
||||
<button type="button" :class="{ active: loginMode === 'password' }" @click="switchLoginMode('password')">密码登录</button>
|
||||
<button type="button" :class="{ active: loginMode === 'mobile' }" @click="switchLoginMode('mobile')">验证码登录</button>
|
||||
</div>
|
||||
|
||||
<el-form ref="loginForm" :model="loginForm" :rules="activeLoginRules" label-position="top">
|
||||
<template v-if="loginMode === 'password'">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model.trim="loginForm.username" placeholder="请输入账户" prefix-icon="el-icon-user" @keyup.enter.native="handleLogin" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
:type="loginPwdVisible ? 'text' : 'password'"
|
||||
placeholder="请输入密码"
|
||||
prefix-icon="el-icon-lock"
|
||||
@keyup.enter.native="handleLogin"
|
||||
>
|
||||
<i slot="suffix" class="el-icon-view password-eye" @click="loginPwdVisible = !loginPwdVisible"></i>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-form-item prop="mobile">
|
||||
<el-input v-model.trim="loginForm.mobile" placeholder="请输入手机号" prefix-icon="el-icon-mobile-phone" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="vcode">
|
||||
<div class="code-row">
|
||||
<el-input v-model.trim="loginForm.vcode" placeholder="请输入验证码" prefix-icon="el-icon-key" @keyup.enter.native="handleLogin" />
|
||||
<button type="button" class="code-btn" :disabled="loginCodeDisabled" @click="getLoginCode">{{ loginCodeText }}</button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="text-btn" @click="forgotPasswordVisible = true">忘记密码?</button>
|
||||
<button type="button" class="text-btn" @click="switchTab('register')">没有账号?去注册</button>
|
||||
</div>
|
||||
|
||||
<button type="button" class="main-btn" :disabled="loginLoading" @click="handleLogin">
|
||||
{{ loginLoading ? '登录中...' : '立即登录' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="form-panel">
|
||||
<el-form ref="registerForm" :model="registerForm" :rules="registerRules" label-position="top">
|
||||
<el-form-item prop="mobile">
|
||||
<el-input v-model.trim="registerForm.mobile" class="phone-input" placeholder="请输入手机号">
|
||||
<span slot="prefix" class="country-prefix">+86</span>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="vcode">
|
||||
<div class="code-row">
|
||||
<el-input v-model.trim="registerForm.vcode" placeholder="请输入验证码" prefix-icon="el-icon-key" />
|
||||
<button type="button" class="code-btn" :disabled="regCodeDisabled" @click="getRegisterCode">{{ regCodeText }}</button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model.trim="registerForm.username" placeholder="请输入账户名" prefix-icon="el-icon-user" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="registerForm.password"
|
||||
:type="regPwdVisible ? 'text' : 'password'"
|
||||
placeholder="请输入密码"
|
||||
prefix-icon="el-icon-lock"
|
||||
@keyup.enter.native="handleRegister"
|
||||
>
|
||||
<i slot="suffix" class="el-icon-view password-eye" @click="regPwdVisible = !regPwdVisible"></i>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="agreement-wrap">
|
||||
<el-checkbox v-model="registerForm.agree">
|
||||
我已阅读并同意《用户协议》、《隐私政策》、《产品服务协议》
|
||||
</el-checkbox>
|
||||
</div>
|
||||
|
||||
<button type="button" class="main-btn" :disabled="registerLoading" @click="handleRegister">
|
||||
{{ registerLoading ? '注册中...' : '立即注册' }}
|
||||
</button>
|
||||
|
||||
<button type="button" class="bottom-link" @click="switchTab('login')">已有账号?前往登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<forgot-password-dialog :visible.sync="forgotPasswordVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { JSEncrypt } from 'jsencrypt'
|
||||
import router, { resetRouter } from '@/router'
|
||||
import { getCodeAPI, logintypeAPI } from '@/api/login'
|
||||
import { register, sendCode } from '@/api/login'
|
||||
import { getHomePath } from '@/views/setting/tools'
|
||||
import ForgotPasswordDialog from '@/views/login/components/ForgotPasswordDialog.vue'
|
||||
|
||||
export default {
|
||||
name: 'LoginDialog',
|
||||
components: { ForgotPasswordDialog },
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const validatePhone = (rule, value, callback) => {
|
||||
if (!value) callback(new Error('请输入手机号'))
|
||||
else if (!/^1[3-9]\d{9}$/.test(value)) callback(new Error('请输入正确的手机号'))
|
||||
else callback()
|
||||
}
|
||||
|
||||
return {
|
||||
activeTab: 'login',
|
||||
loginMode: 'password',
|
||||
forgotPasswordVisible: false,
|
||||
loginLoading: false,
|
||||
registerLoading: false,
|
||||
loginPwdVisible: false,
|
||||
regPwdVisible: false,
|
||||
loginCodeDisabled: false,
|
||||
regCodeDisabled: false,
|
||||
loginCodeText: '获取验证码',
|
||||
regCodeText: '获取验证码',
|
||||
loginTimer: null,
|
||||
regTimer: null,
|
||||
loginCount: 60,
|
||||
regCount: 60,
|
||||
loginForm: {
|
||||
username: '',
|
||||
password: '',
|
||||
mobile: '',
|
||||
vcode: '',
|
||||
codeid: ''
|
||||
},
|
||||
registerForm: {
|
||||
mobile: '',
|
||||
vcode: '',
|
||||
codeid: '',
|
||||
username: '',
|
||||
password: '',
|
||||
org_type: '2',
|
||||
agree: false,
|
||||
wechat_openid: localStorage.getItem('wechat_openid') || '',
|
||||
domain_name: window.location.hostname
|
||||
},
|
||||
loginRules: {
|
||||
username: [{ required: true, message: '请输入账户', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||
mobile: [{ required: true, validator: validatePhone, trigger: 'blur' }],
|
||||
vcode: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
|
||||
},
|
||||
registerRules: {
|
||||
mobile: [{ required: true, validator: validatePhone, trigger: 'blur' }],
|
||||
vcode: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
|
||||
username: [{ required: true, message: '请输入账户名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dialogVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:visible', value)
|
||||
}
|
||||
},
|
||||
activeLoginRules() {
|
||||
if (this.loginMode === 'mobile') {
|
||||
return {
|
||||
mobile: this.loginRules.mobile,
|
||||
vcode: this.loginRules.vcode
|
||||
}
|
||||
}
|
||||
return {
|
||||
username: this.loginRules.username,
|
||||
password: this.loginRules.password
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.loginTimer)
|
||||
clearInterval(this.regTimer)
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.loginLoading = false
|
||||
this.registerLoading = false
|
||||
this.$emit('close')
|
||||
},
|
||||
switchTab(tab) {
|
||||
this.activeTab = tab
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.loginForm) this.$refs.loginForm.clearValidate()
|
||||
if (this.$refs.registerForm) this.$refs.registerForm.clearValidate()
|
||||
})
|
||||
},
|
||||
switchLoginMode(mode) {
|
||||
this.loginMode = mode
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.loginForm) this.$refs.loginForm.clearValidate()
|
||||
})
|
||||
},
|
||||
passwordEncryption(passwordUser) {
|
||||
const publicKey = '-----BEGIN PUBLIC KEY-----\n' +
|
||||
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApJ3ThUWT3CgvH0O8rrT6\n' +
|
||||
'qycpqX0NTq4Q3CxBrvNxo9//qX2bKvhomoLNd+vdti8xNOK6/3zuTJIVt0RoNKwE\n' +
|
||||
'0HWMR8H0jgp7ING54DtT5B8bhmUpbs/hownGzIBGOedhqeOPiv0Q5oSi9OIEE+PK\n' +
|
||||
'2L8KdgFF2Z6Q1DQdv5Y1qvD/t2mJVjR+NPTwcwBIT8UJ0Cfu8lqHjjJbNF//smTj\n' +
|
||||
'Q8v2pnqp19jItuHeD4G4u7a8fWC3/IGEv4+uc5rq5qhwdzRxHUveNmoE+nyh0T8R\n' +
|
||||
'C8Y8/XLkEiD0nMvZZjBn7Bof6f1st0aqJX8R1VGvzdTJ8eTvJuyMNsR4wLoF5Pvx\n' +
|
||||
'hQIDAQAB\n' +
|
||||
'-----END PUBLIC KEY-----'
|
||||
const encryptor = new JSEncrypt()
|
||||
encryptor.setPublicKey(publicKey)
|
||||
return encryptor.encrypt(passwordUser)
|
||||
},
|
||||
buildLoginParams() {
|
||||
const commonParams = {
|
||||
domain_name: window.location.hostname,
|
||||
wechat_openid: ''
|
||||
}
|
||||
if (this.loginMode === 'mobile') {
|
||||
return {
|
||||
...commonParams,
|
||||
mobile: this.loginForm.mobile,
|
||||
vcode: this.loginForm.vcode,
|
||||
codeid: this.loginForm.codeid
|
||||
}
|
||||
}
|
||||
return {
|
||||
...commonParams,
|
||||
username: this.loginForm.username,
|
||||
password: this.passwordEncryption(this.loginForm.password)
|
||||
}
|
||||
},
|
||||
startCountdown(type) {
|
||||
const isLogin = type === 'login'
|
||||
const timerKey = isLogin ? 'loginTimer' : 'regTimer'
|
||||
const countKey = isLogin ? 'loginCount' : 'regCount'
|
||||
const textKey = isLogin ? 'loginCodeText' : 'regCodeText'
|
||||
const disabledKey = isLogin ? 'loginCodeDisabled' : 'regCodeDisabled'
|
||||
|
||||
clearInterval(this[timerKey])
|
||||
this[countKey] = 59
|
||||
this[disabledKey] = true
|
||||
this[textKey] = `重新发送 ${this[countKey]}s`
|
||||
this[timerKey] = setInterval(() => {
|
||||
if (this[countKey] > 0) {
|
||||
this[countKey] -= 1
|
||||
this[textKey] = `重新发送 ${this[countKey]}s`
|
||||
return
|
||||
}
|
||||
clearInterval(this[timerKey])
|
||||
this[timerKey] = null
|
||||
this[countKey] = 60
|
||||
this[disabledKey] = false
|
||||
this[textKey] = '获取验证码'
|
||||
}, 1000)
|
||||
},
|
||||
getLoginCode() {
|
||||
this.$refs.loginForm.validateField('mobile', async error => {
|
||||
if (error) return
|
||||
try {
|
||||
const res = await getCodeAPI({ mobile: this.loginForm.mobile, action_type: 'login' })
|
||||
if (res.status) {
|
||||
this.loginForm.codeid = res.codeid || (res.data && res.data.codeid) || ''
|
||||
this.startCountdown('login')
|
||||
this.$message.success('验证码已发送')
|
||||
} else {
|
||||
this.$message.error(res.msg || '验证码获取失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('验证码获取失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
getRegisterCode() {
|
||||
this.$refs.registerForm.validateField('mobile', async error => {
|
||||
if (error) return
|
||||
try {
|
||||
const res = await sendCode({ mobile: this.registerForm.mobile, action_type: 'register' })
|
||||
if (res.status) {
|
||||
this.registerForm.codeid = res.codeid || (res.data && res.data.codeid) || res.data || ''
|
||||
this.startCountdown('register')
|
||||
this.$message.success('验证码已发送')
|
||||
} else {
|
||||
this.$message.error(res.msg || '验证码获取失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('验证码获取失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
handleLogin() {
|
||||
this.$refs.loginForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
const loginParams = this.buildLoginParams()
|
||||
this.loginLoading = true
|
||||
try {
|
||||
const check = await logintypeAPI(loginParams)
|
||||
if (!check.status) {
|
||||
this.$message.error(check.msg || '登录失败')
|
||||
return
|
||||
}
|
||||
const res = await this.$store.dispatch('user/login', loginParams)
|
||||
if (!res.status) {
|
||||
this.$message.error(res.msg || '登录失败')
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem('user_info', JSON.stringify(res.user))
|
||||
sessionStorage.setItem('userId', res.userId)
|
||||
sessionStorage.setItem('orgid', res.user.orgid)
|
||||
sessionStorage.setItem('org_type', res.org_type)
|
||||
sessionStorage.setItem('username', res.user.username)
|
||||
|
||||
if (res.admin !== 1) {
|
||||
sessionStorage.setItem('juese', res.roles[0])
|
||||
sessionStorage.setItem('jueseNew', res.roles)
|
||||
} else {
|
||||
sessionStorage.setItem('juese', 'admin')
|
||||
sessionStorage.setItem('jueseNew', 'admin')
|
||||
}
|
||||
sessionStorage.setItem('roles', JSON.stringify(res.admin !== 1 ? res.roles : ['admin']))
|
||||
|
||||
this.$store.commit('setLoginState', true)
|
||||
resetRouter()
|
||||
this.$store.commit('permission/RESET_ROUTES')
|
||||
const routeRoles = res.admin !== 1 ? res.roles : ['admin']
|
||||
const accessRoutes = await this.$store.dispatch('permission/generateRoutes', {
|
||||
user: res.user.username,
|
||||
auths: res.data,
|
||||
admin: res.admin || '',
|
||||
orgType: res.org_type,
|
||||
roles: routeRoles
|
||||
})
|
||||
router.addRoutes(accessRoutes)
|
||||
|
||||
this.$message.success(res.msg || '登录成功')
|
||||
this.dialogVisible = false
|
||||
this.$emit('success', res)
|
||||
this.redirectAfterLogin(res)
|
||||
} catch (error) {
|
||||
this.$message.error((error && error.msg) || '登录失败')
|
||||
} finally {
|
||||
this.loginLoading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
redirectAfterLogin(res) {
|
||||
const redirectPath = sessionStorage.getItem('loginRedirectPath')
|
||||
if (redirectPath && redirectPath.startsWith('/') && !redirectPath.includes('/login')) {
|
||||
sessionStorage.removeItem('loginRedirectPath')
|
||||
this.$router.push(redirectPath).catch(() => {})
|
||||
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(() => {})
|
||||
},
|
||||
handleRegister() {
|
||||
if (!this.registerForm.agree) {
|
||||
this.$message.warning('请先阅读并同意相关协议')
|
||||
return
|
||||
}
|
||||
this.$refs.registerForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
this.registerLoading = true
|
||||
try {
|
||||
const registerData = {
|
||||
mobile: this.registerForm.mobile,
|
||||
vcode: this.registerForm.vcode,
|
||||
codeid: this.registerForm.codeid,
|
||||
org_type: '2',
|
||||
username: this.registerForm.username || this.registerForm.mobile,
|
||||
password: this.registerForm.password,
|
||||
nick_name: this.registerForm.mobile,
|
||||
wechat_openid: this.registerForm.wechat_openid,
|
||||
domain_name: this.registerForm.domain_name || window.location.hostname
|
||||
}
|
||||
const res = await register(registerData)
|
||||
if (res.status) {
|
||||
this.$message.success('注册成功,请登录')
|
||||
this.loginForm.mobile = this.registerForm.mobile
|
||||
this.switchTab('login')
|
||||
this.loginMode = 'mobile'
|
||||
this.$refs.registerForm.resetFields()
|
||||
this.registerForm.agree = false
|
||||
} else {
|
||||
this.$message.error(res.message || res.msg || '注册失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('注册失败,请重试')
|
||||
} finally {
|
||||
this.registerLoading = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
::v-deep .glass-dialog {
|
||||
overflow: hidden;
|
||||
// border: 1px solid rgba(255, 255, 255, 0.9);
|
||||
border-radius: 28px;
|
||||
background: #fff;
|
||||
box-shadow:
|
||||
0 30px 80px rgba(15, 23, 42, 0.16),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(28px) saturate(1.35);
|
||||
-webkit-backdrop-filter: blur(28px) saturate(1.35);
|
||||
|
||||
.el-dialog__header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
right: 28px;
|
||||
z-index: 10;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
}
|
||||
|
||||
.el-dialog__close {
|
||||
color: #1f2937;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-card {
|
||||
position: relative;
|
||||
padding: 40px 44px 36px;
|
||||
}
|
||||
|
||||
.brand-area {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.brand-badge {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.5px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(135deg, #2563eb, #7c3aed);
|
||||
box-shadow: 0 14px 30px rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
|
||||
.brand-text h2 {
|
||||
margin: 0;
|
||||
color: #020817;
|
||||
font-size: 30px;
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.04em;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.brand-text p {
|
||||
margin: 4px 0 0;
|
||||
color: #263548;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tab-group {
|
||||
display: flex;
|
||||
height: 52px;
|
||||
padding: 0;
|
||||
margin-bottom: 34px;
|
||||
background: transparent;
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.26);
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.tab-group button {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.tab-group button.active {
|
||||
color: #020617;
|
||||
background: transparent;
|
||||
border-bottom-color: #020617;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.mode-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.mode-tabs button {
|
||||
height: 46px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border: 1px solid rgba(15, 23, 42, 0.72);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.66);
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.mode-tabs button.active {
|
||||
color: #020617;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-color: #020617;
|
||||
}
|
||||
|
||||
.form-panel ::v-deep .el-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-panel ::v-deep .el-form-item__error {
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-panel ::v-deep .el-input__inner {
|
||||
height: 48px;
|
||||
padding-left: 44px;
|
||||
border: 1px solid #c8d3e2;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
color: #020817;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.form-panel ::v-deep .el-input__inner:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.12);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.form-panel ::v-deep .el-input__inner::placeholder {
|
||||
color: #64748b;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.form-panel ::v-deep .el-input__prefix {
|
||||
left: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #475569;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.form-panel ::v-deep .el-input__suffix {
|
||||
right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.password-eye {
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
|
||||
.code-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.code-row .el-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.code-btn {
|
||||
height: 48px;
|
||||
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;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 2px 0 20px;
|
||||
}
|
||||
|
||||
.text-btn,
|
||||
.bottom-link {
|
||||
padding: 4px 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #1f2937;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #2563eb;
|
||||
}
|
||||
}
|
||||
|
||||
.main-btn {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
// background: linear-gradient(90deg, #2463f6, #24b9f1);
|
||||
background-color: #020617;
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 14px 32px rgba(37, 99, 235, 0.32);
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.main-btn:hover:not(:disabled) {
|
||||
box-shadow: 0 18px 40px rgba(37, 99, 235, 0.34);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.main-btn:disabled {
|
||||
opacity: 0.72;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.country-prefix {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-left: 12px;
|
||||
color: #1f2937;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-panel ::v-deep .phone-input .el-input__prefix {
|
||||
left: 0;
|
||||
width: 54px;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-panel ::v-deep .phone-input .el-input__inner {
|
||||
padding-left: 58px;
|
||||
}
|
||||
|
||||
.agreement-wrap {
|
||||
margin: 4px 0 20px;
|
||||
|
||||
::v-deep .el-checkbox__label {
|
||||
color: #1f2937;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
::v-deep .glass-dialog {
|
||||
width: calc(100vw - 28px) !important;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding: 32px 22px 28px;
|
||||
}
|
||||
|
||||
.brand-badge {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
font-size: 20px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.brand-text h2 {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.brand-text p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tab-group {
|
||||
height: 52px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tab-group button {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.mode-tabs {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.mode-tabs button {
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -51,16 +51,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="nav-item" @click.stop="goHomeAnchor('cases-section')">
|
||||
<button v-if="!isNcmatchDomain" type="button" class="nav-item" @click.stop="goHomeAnchor('cases-section')">
|
||||
{{ $t('topbar.cases') }}
|
||||
</button>
|
||||
<button type="button" class="nav-item" @click.stop="goHomeAnchor('news')">
|
||||
<button v-if="!isNcmatchDomain" type="button" class="nav-item" @click.stop="goHomeAnchor('news')">
|
||||
{{ $t('topbar.news') }}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="user-actions">
|
||||
<button
|
||||
v-if="!isNcmatchDomain"
|
||||
type="button"
|
||||
class="nav-lang-text"
|
||||
:class="{ 'is-en': activeLocale === 'en-US' }"
|
||||
@ -76,7 +77,7 @@
|
||||
{{ $t('topbar.console') }}
|
||||
</button>
|
||||
|
||||
<button v-if="!loginState" type="button" class="login-btn" @click.stop="goLogin">
|
||||
<button v-if="!loginState" type="button" class="login-btn" @click.stop="openLoginDialog">
|
||||
{{ $t('topbar.login') }}
|
||||
</button>
|
||||
|
||||
@ -224,6 +225,12 @@
|
||||
:user-id="userId"
|
||||
@unread-count-update="handleUnreadCountUpdate"
|
||||
/>
|
||||
|
||||
<login-dialog
|
||||
:visible.sync="loginDialogVisible"
|
||||
@success="handleLoginDialogSuccess"
|
||||
@forgot-password="goLogin"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -237,13 +244,14 @@ import { reqAIChat } from '@/api/AI/ai'
|
||||
import { gotoYuanJingAPI } from '@/api/gotoYuanJing'
|
||||
import { setLocale } from '@/i18n'
|
||||
import MessageCenter from '@/components/MessageCenter/MessageCenter.vue'
|
||||
import LoginDialog from '@/views/LoginDialog/LoginDialog.vue'
|
||||
|
||||
const LOCALE_KEY = 'kboss-locale'
|
||||
const SUPPORTED_LOCALES = ['zh-CN', 'en-US']
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'TopBox',
|
||||
components: { MessageCenter },
|
||||
components: { MessageCenter, LoginDialog },
|
||||
data() {
|
||||
return {
|
||||
// AI 聊天面板状态
|
||||
@ -266,6 +274,7 @@ export default Vue.extend({
|
||||
messageCount: 0,
|
||||
activeLocale: 'zh-CN',
|
||||
isProductPanelVisible: false,
|
||||
loginDialogVisible: false,
|
||||
|
||||
// 登录信息
|
||||
isShowKbossCharge: false,
|
||||
@ -494,6 +503,18 @@ export default Vue.extend({
|
||||
// =========================
|
||||
// 登录、用户与消息
|
||||
// =========================
|
||||
openLoginDialog() {
|
||||
this.closeProductPanelImmediate()
|
||||
if (this.$route && this.$route.fullPath && !this.$route.fullPath.includes('/login')) {
|
||||
sessionStorage.setItem('loginRedirectPath', this.$route.fullPath)
|
||||
}
|
||||
this.loginDialogVisible = true
|
||||
},
|
||||
handleLoginDialogSuccess() {
|
||||
this.loginDialogVisible = false
|
||||
this.nick_name = sessionStorage.getItem('username') || ''
|
||||
this.userId = sessionStorage.getItem('userId')
|
||||
},
|
||||
goLogin() {
|
||||
this.closeProductPanelImmediate()
|
||||
if (this.$route && this.$route.fullPath && !this.$route.fullPath.includes('/login')) {
|
||||
@ -528,12 +549,6 @@ export default Vue.extend({
|
||||
else if (role.includes('admin')) this.$router.push('/superAdministrator/addAdmin')
|
||||
},
|
||||
async logout() {
|
||||
let redirectPath = ''
|
||||
if (this.$route && this.$route.fullPath && !this.$route.fullPath.includes('/login')) {
|
||||
redirectPath = this.$route.fullPath
|
||||
sessionStorage.setItem('loginRedirectPath', redirectPath)
|
||||
}
|
||||
|
||||
this.$store.commit('setLoginState', false)
|
||||
store.commit('tagsView/resetBreadcrumbState')
|
||||
store.commit('permission/RESET_ROUTES')
|
||||
@ -548,17 +563,17 @@ export default Vue.extend({
|
||||
sessionStorage.removeItem('roles')
|
||||
sessionStorage.removeItem('juese')
|
||||
sessionStorage.removeItem('jueseNew')
|
||||
sessionStorage.removeItem('loginRedirectPath')
|
||||
|
||||
localStorage.removeItem('auths')
|
||||
localStorage.removeItem('routes')
|
||||
localStorage.removeItem('user')
|
||||
localStorage.removeItem('userId')
|
||||
localStorage.removeItem('org_type')
|
||||
this.userId = ''
|
||||
this.nick_name = ''
|
||||
|
||||
await this.$router.push({
|
||||
path: '/login',
|
||||
query: redirectPath ? { redirect: redirectPath } : {}
|
||||
})
|
||||
await this.$router.replace(getHomePath()).catch(() => {})
|
||||
},
|
||||
initMybalance() {
|
||||
return sessionStorage.getItem('mybalance')
|
||||
@ -928,7 +943,7 @@ export default Vue.extend({
|
||||
|
||||
.login-btn {
|
||||
height: 48px;
|
||||
width: 100px;
|
||||
width: 86px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 99px;
|
||||
|
||||
@ -155,6 +155,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reqNewsList } from '@/api/newsapi/newsapi'
|
||||
import Talk from '@/views/homePage/dialog/talk/index.vue'
|
||||
|
||||
export default {
|
||||
@ -193,138 +194,123 @@ export default {
|
||||
image: require('./img/case/beigang-big-data-case.jpg')
|
||||
}
|
||||
],
|
||||
newsList: [
|
||||
{
|
||||
tag: '企业动态',
|
||||
tagClass: 'news-tag--blue',
|
||||
date: '2026.6.15',
|
||||
title: '开元云随贸促会走访东盟',
|
||||
desc: '开元云科技随贸促会广西分会经贸代表团密集出访越南、老挝,深度参与区域 AI 产业合作与数字经济交流。'
|
||||
},
|
||||
{
|
||||
tag: '企业动态',
|
||||
tagClass: 'news-tag--green',
|
||||
date: '2026.05.17',
|
||||
title: '开元云荣登福布斯中国人工智能商业落地示范企业',
|
||||
desc: '凭借 AI 智能体工厂的工业级交付能力与央国企标杆案例,开元云入选福布斯中国人工智能商业落地示范企业。'
|
||||
}
|
||||
],
|
||||
newsList: [],
|
||||
solutionCards: [
|
||||
{
|
||||
title: '群智协作',
|
||||
subtitle: '多智能体协同',
|
||||
detailTitle: '慧投标智能体',
|
||||
detailSubTitle: '基于 Agent+RAG+LangGraph 多节点编排,实现招投标全流程 AI 化',
|
||||
detailDesc: '从标书解读、资质匹配到内容生成,多智能体协同提升投标作业效率。',
|
||||
detailTitle: '群智协作',
|
||||
detailSubTitle: '多智能体协同',
|
||||
detailDesc: '多个AI智能体按角色分工,自主完成信息采集、数据分析、逻辑推理与结论生成的全流程协作。支持动态任务分配与上下文共享,将复杂业务决策从"人串联"变为"智能体并行",大幅压缩高知识密度工作的完成周期。',
|
||||
metrics: [
|
||||
{ value: '10倍+', label: '效率提升' },
|
||||
{ value: '80%', label: '废标率降低' },
|
||||
{ value: '35%', label: '中标率提升' }
|
||||
{ value: '10倍+', label: '知识工作效率提升' },
|
||||
{ value: '多源', label: '信息实时融合' },
|
||||
{ value: '高复杂度', label: '决策场景适配' }
|
||||
],
|
||||
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)',
|
||||
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>'
|
||||
},
|
||||
{
|
||||
title: '空间觉醒',
|
||||
subtitle: 'GIS智能分析',
|
||||
detailTitle: '空间觉醒',
|
||||
detailSubTitle: 'GIS智能分析',
|
||||
detailDesc: '融合地理信息系统、卫星遥感影像、无人机航拍等多维空间数据,AI自动识别地物边界、叠加分析空间关系、校验合规约束。将传统需要人工逐层比对的地理空间判断,升级为分钟级自动完成的空间智能决策。',
|
||||
metrics: [
|
||||
{ value: '分钟级', label: '空间分析' },
|
||||
{ value: '90%+', label: '违规识别准确率' },
|
||||
{ value: '70%', label: '人力成本节省' }
|
||||
],
|
||||
bg: 'linear-gradient(145deg, rgba(200, 238, 218, 0.72) 0%, rgba(152, 216, 184, 0.64) 44%, rgba(104, 194, 150, 0.58) 100%)',
|
||||
shadow: '0 18px 54px rgba(60, 150, 100, 0.18), 0 4px 16px rgba(60, 150, 100, 0.08)',
|
||||
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="24" r="18"/><ellipse cx="24" cy="24" rx="10" ry="18"/><line x1="6" y1="24" x2="42" y2="24"/><path d="M8 16h32"/><path d="M8 32h32"/><path d="M24 6v36"/></svg>'
|
||||
},
|
||||
{
|
||||
title: '先见一步',
|
||||
subtitle: '智能预测引擎',
|
||||
detailTitle: '先见一步',
|
||||
detailSubTitle: '智能预测引擎',
|
||||
detailDesc: '融合机理模型与数据驱动模型,通过实时采集设备运行参数,AI自主识别早期异常特征并精准预测剩余使用寿命。在故障真正发生前72小时发出预警,让运维从"事后抢修"转变为"事前防御",最大化保障生产连续性。',
|
||||
metrics: [
|
||||
{ value: '95%+', label: '故障预警准确率' },
|
||||
{ value: '60%', label: '非计划停机减少' },
|
||||
{ value: '72小时', label: '预测提前量' }
|
||||
],
|
||||
bg: 'linear-gradient(145deg, rgba(214, 240, 224, 0.72) 0%, rgba(168, 222, 190, 0.64) 44%, rgba(122, 204, 158, 0.58) 100%)',
|
||||
shadow: '0 18px 54px rgba(60, 150, 100, 0.18), 0 4px 16px rgba(60, 150, 100, 0.08)',
|
||||
icon: '<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 38l10-12 8 6 10-16 8 10"/><polyline points="36,22 42,26 38,32"/><line x1="6" y1="40" x2="42" y2="40"/><path d="M6 8v32"/></svg>'
|
||||
},
|
||||
{
|
||||
title: '洞察入微',
|
||||
subtitle: '机器视觉检测',
|
||||
detailTitle: '洞察入微',
|
||||
detailSubTitle: '机器视觉检测',
|
||||
detailDesc: '基于深度学习的高速产线全幅面缺陷检测能力,覆盖表面处理、涂层喷涂、焊接装配等全流程质检环节。不是抽样检查,而是每一寸产品都经过AI逐像素比对,在毫秒级时间内完成缺陷识别与分类,确保出厂品质零妥协。',
|
||||
metrics: [
|
||||
{ value: '毫秒级', label: '检测速度' },
|
||||
{ value: '95%', label: '漏检率降低' },
|
||||
{ value: '99.5%+', label: '识别准确率' }
|
||||
],
|
||||
bg: 'linear-gradient(145deg, rgba(192, 222, 239, 0.72) 0%, rgba(136, 194, 224, 0.64) 44%, rgba(80, 166, 208, 0.58) 100%)',
|
||||
shadow: '0 18px 54px rgba(40, 110, 170, 0.18), 0 4px 16px rgba(40, 110, 170, 0.08)',
|
||||
icon: '<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 24s8-14 20-14 20 14 20 14-8 14-20 14S4 24 4 24z"/><circle cx="24" cy="24" r="7"/><circle cx="24" cy="24" r="3"/><line x1="24" y1="6" x2="24" y2="10"/><line x1="24" y1="38" x2="24" y2="42"/></svg>'
|
||||
},
|
||||
{
|
||||
title: '一问即达',
|
||||
subtitle: '智能问答知识库',
|
||||
detailTitle: '智能问答知识库',
|
||||
detailSubTitle: '燃机 AI 诊断助手',
|
||||
detailDesc: '融合专家知识库,支持中英文自然语言多轮对话,自动生成诊断建议。',
|
||||
detailTitle: '一问即达',
|
||||
detailSubTitle: '智能问答知识库',
|
||||
detailDesc: '基于企业私有知识库构建的AI问答引擎,支持自然语言直接查询业务数据、技术规范、制度文件。每条回答自动标注权威来源,信息可追溯、可验证。让专家级知识不再锁在文档里,而是像问同事一样即问即答。',
|
||||
metrics: [
|
||||
{ value: '<3s', label: '诊断响应' },
|
||||
{ value: '90%', label: '知识覆盖' },
|
||||
{ value: '<3秒', label: '响应速度' },
|
||||
{ value: '90%', label: '专家知识覆盖率' },
|
||||
{ value: '5倍', label: '决策效率提升' }
|
||||
],
|
||||
bg: 'linear-gradient(145deg, rgba(224, 208, 242, 0.72) 0%, rgba(200, 176, 230, 0.64) 44%, rgba(170, 142, 216, 0.58) 100%)',
|
||||
shadow: '0 18px 54px rgba(130, 80, 180, 0.18), 0 4px 16px rgba(130, 80, 180, 0.08)',
|
||||
icon: '<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="8" width="36" height="28" rx="4"/><path d="M6 16h36"/><circle cx="24" cy="28" r="3"/><path d="M18 28h1"/><path d="M29 28h1"/><path d="M18 42h12"/></svg>'
|
||||
},
|
||||
{
|
||||
title: '空间觉醒',
|
||||
subtitle: 'GIS智能分析',
|
||||
detailTitle: '林业采伐空间智审',
|
||||
detailSubTitle: 'AI+GIS 深度融合,智能识别禁伐区、历史采伐点',
|
||||
detailDesc: '输出合规性报告,辅助采伐审批与空间治理。',
|
||||
metrics: [
|
||||
{ value: '分钟级', label: '审批周期' },
|
||||
{ value: '90%', label: '违规识别提升' },
|
||||
{ value: '70%', label: '节省人力' }
|
||||
],
|
||||
bg: 'linear-gradient(145deg, rgba(200, 238, 218, 0.72) 0%, rgba(152, 216, 184, 0.64) 44%, rgba(104, 194, 150, 0.58) 100%)',
|
||||
shadow: '0 18px 54px rgba(60, 150, 100, 0.18), 0 4px 16px rgba(60, 150, 100, 0.08)',
|
||||
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="24" r="18"/><ellipse cx="24" cy="24" rx="10" ry="18"/><line x1="6" y1="24" x2="42" y2="24"/><path d="M8 16h32"/><path d="M8 32h32"/><path d="M24 6v36"/></svg>'
|
||||
},
|
||||
{
|
||||
title: '全程守护',
|
||||
subtitle: '设备智能监控',
|
||||
detailTitle: '三维可视化监盘系统',
|
||||
detailSubTitle: '自动告警高亮定位,降低运维人员技能门槛',
|
||||
detailDesc: '一机一策三维可视化,实时监控设备关键点位。',
|
||||
detailTitle: '全程守护',
|
||||
detailSubTitle: '设备智能监控',
|
||||
detailDesc: '为每台设备构建独立监控策略,通过三维可视化实时呈现运行状态。AI自动识别异常并高亮定位故障点,同步输出详细诊断建议和排查步骤。将运维人员从"盯屏找问题"解放出来,让系统主动告诉你问题在哪、怎么修。',
|
||||
metrics: [
|
||||
{ value: '1000+', label: '监控点位' },
|
||||
{ value: '<1s', label: '告警响应' },
|
||||
{ value: '40%', label: '成本降低' }
|
||||
{ value: '1000+', label: '监控点位支持' },
|
||||
{ value: '<1秒', label: '告警响应' },
|
||||
{ value: '40%', label: '运维成本降低' }
|
||||
],
|
||||
bg: 'linear-gradient(145deg, rgba(251, 230, 200, 0.72) 0%, rgba(245, 205, 160, 0.64) 44%, rgba(238, 180, 120, 0.58) 100%)',
|
||||
shadow: '0 18px 54px rgba(190, 130, 50, 0.18), 0 4px 16px rgba(190, 130, 50, 0.08)',
|
||||
icon: '<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="10" width="32" height="22" rx="3"/><path d="M18 36h12"/><line x1="24" y1="32" x2="24" y2="36"/><path d="M16 20l4 4 6-8"/><circle cx="38" cy="12" r="4"/></svg>'
|
||||
},
|
||||
{
|
||||
title: '先见一步',
|
||||
subtitle: '智能预测引擎',
|
||||
detailTitle: '燃机智慧监盘',
|
||||
detailSubTitle: 'AI 预测引擎+TDengine 时序数据库',
|
||||
detailDesc: '从被动响应到主动预警,保障设备稳定运行。',
|
||||
metrics: [
|
||||
{ value: '95%+', label: '预警准确率' },
|
||||
{ value: '60%', label: '停机时间减少' },
|
||||
{ value: '72h', label: '提前预警' }
|
||||
],
|
||||
bg: 'linear-gradient(145deg, rgba(214, 240, 224, 0.72) 0%, rgba(168, 222, 190, 0.64) 44%, rgba(122, 204, 158, 0.58) 100%)',
|
||||
shadow: '0 18px 54px rgba(60, 150, 100, 0.18), 0 4px 16px rgba(60, 150, 100, 0.08)',
|
||||
icon: '<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 38l10-12 8 6 10-16 8 10"/><polyline points="36,22 42,26 38,32"/><line x1="6" y1="40" x2="42" y2="40"/><path d="M6 8v32"/></svg>'
|
||||
},
|
||||
{
|
||||
title: '触文即懂',
|
||||
subtitle: '智能文档解析',
|
||||
detailTitle: '招标文件智能解析',
|
||||
detailSubTitle: 'AI 自动读取招标文件,结构化提取关键信息',
|
||||
detailDesc: '精准识别资质要求、评分标准和关键条款,辅助人工快速审查。',
|
||||
detailTitle: '触文即懂',
|
||||
detailSubTitle: '智能文档解析',
|
||||
detailDesc: '支持PDF、Word、扫描件等多种格式一键解析,AI自动提取关键条款、资质要求、评分细则等核心信息并结构化呈现。不是简单的OCR文字识别,而是理解文档语义、判断信息重要性、标注风险点,让冗长文档的核心内容一目了然。',
|
||||
metrics: [
|
||||
{ value: '98%+', label: '解析准确率' },
|
||||
{ value: '85%', label: '核查时间减少' },
|
||||
{ value: '0漏', label: '关键信息' }
|
||||
{ value: '85%', label: '人工核查时间减少' },
|
||||
{ value: '零遗漏', label: '关键信息' }
|
||||
],
|
||||
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)',
|
||||
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>'
|
||||
},
|
||||
{
|
||||
title: '洞察入微',
|
||||
subtitle: '机器视觉检测',
|
||||
detailTitle: '零碳铝板缺陷检测',
|
||||
detailSubTitle: 'YOLO 目标检测+TensorRT 边缘推理',
|
||||
detailDesc: '毫秒级实时质检,替代人工目检,提升产线检测稳定性。',
|
||||
metrics: [
|
||||
{ value: 'ms级', label: '检测速度' },
|
||||
{ value: '95%+', label: '漏检率降低' },
|
||||
{ value: '25%', label: '产能提升' }
|
||||
],
|
||||
bg: 'linear-gradient(145deg, rgba(192, 222, 239, 0.72) 0%, rgba(136, 194, 224, 0.64) 44%, rgba(80, 166, 208, 0.58) 100%)',
|
||||
shadow: '0 18px 54px rgba(40, 110, 170, 0.18), 0 4px 16px rgba(40, 110, 170, 0.08)',
|
||||
icon: '<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 24s8-14 20-14 20 14 20 14-8 14-20 14S4 24 4 24z"/><circle cx="24" cy="24" r="7"/><circle cx="24" cy="24" r="3"/><line x1="24" y1="6" x2="24" y2="10"/><line x1="24" y1="38" x2="24" y2="42"/></svg>'
|
||||
},
|
||||
{
|
||||
title: '端侧即算',
|
||||
subtitle: '边缘AI推理',
|
||||
detailTitle: '边缘实时质检',
|
||||
detailSubTitle: 'TensorRT 优化模型,边缘端毫秒级推理',
|
||||
detailDesc: '无需上传云端,保障数据安全并适配 7x24 小时连续运行。',
|
||||
detailTitle: '端侧即算',
|
||||
detailSubTitle: '边缘AI推理',
|
||||
detailDesc: '将AI推理能力部署在生产现场边缘侧,通过轻量化算法实现毫秒级实时决策。数据无需回传云端,在本地即可完成从感知到判断的全流程,确保数据不出厂、响应零延迟。冗余架构设计保障7×24小时持续运行,产线检测永不中断。',
|
||||
metrics: [
|
||||
{ value: '<50ms', label: '推理延迟' },
|
||||
{ value: '本地', label: '数据不出厂' },
|
||||
{ value: '7x24', label: '持续运行' }
|
||||
{ value: '本地', label: '数据处理不出厂' },
|
||||
{ value: '7×24h', label: '持续运行' }
|
||||
],
|
||||
bg: 'linear-gradient(145deg, rgba(216, 204, 240, 0.72) 0%, rgba(184, 164, 226, 0.64) 44%, rgba(150, 124, 212, 0.58) 100%)',
|
||||
shadow: '0 18px 54px rgba(90, 60, 160, 0.18), 0 4px 16px rgba(90, 60, 160, 0.08)',
|
||||
@ -372,17 +358,7 @@ export default {
|
||||
})
|
||||
},
|
||||
localizedNewsList() {
|
||||
const keys = ['first', 'second']
|
||||
return this.newsList.map((item, index) => {
|
||||
const key = keys[index]
|
||||
if (!key) return item
|
||||
return {
|
||||
...item,
|
||||
tag: this.$t('home.news.tag'),
|
||||
title: this.$t(`home.news.${key}Title`),
|
||||
desc: this.$t(`home.news.${key}Desc`)
|
||||
}
|
||||
})
|
||||
return this.newsList
|
||||
},
|
||||
visibleSolutionCards() {
|
||||
const slots = [-3, -2, -1, 0, 1, 2, 3]
|
||||
@ -408,6 +384,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.startSolutionCarousel()
|
||||
this.getHomeNewsList()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopSolutionCarousel()
|
||||
@ -442,6 +419,48 @@ export default {
|
||||
goNews() {
|
||||
this.navigateTo('/homePage/news')
|
||||
},
|
||||
buildHomeNewsParams() {
|
||||
return {
|
||||
url_link: window.location.href,
|
||||
current_page: '1',
|
||||
page_size: '2',
|
||||
title: '',
|
||||
article_type: '',
|
||||
status: '1'
|
||||
}
|
||||
},
|
||||
getNewsTagClass(index) {
|
||||
return index % 2 === 0 ? 'news-tag--blue' : 'news-tag--green'
|
||||
},
|
||||
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 || ''
|
||||
}
|
||||
},
|
||||
getResponseList(res) {
|
||||
if (Array.isArray(res.data)) return res.data
|
||||
if (res.data && Array.isArray(res.data.data)) return res.data.data
|
||||
if (res.data && Array.isArray(res.data.list)) return res.data.list
|
||||
if (Array.isArray(res.list)) return res.list
|
||||
return []
|
||||
},
|
||||
async getHomeNewsList() {
|
||||
try {
|
||||
const res = await reqNewsList(this.buildHomeNewsParams())
|
||||
if (res && (res.status === true || res.status === 'true')) {
|
||||
this.newsList = this.getResponseList(res)
|
||||
.slice(0, 2)
|
||||
.map((item, index) => this.normalizeHomeNewsItem(item, index))
|
||||
}
|
||||
} catch (error) {
|
||||
this.newsList = []
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
nextSolution() {
|
||||
|
||||
592
f/web-kboss/src/views/homePage/news/ArticleEditorDialog.vue
Normal file
592
f/web-kboss/src/views/homePage/news/ArticleEditorDialog.vue
Normal file
@ -0,0 +1,592 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:visible.sync="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
:fullscreen="true"
|
||||
append-to-body
|
||||
custom-class="article-editor-dialog"
|
||||
:close-on-click-modal="false"
|
||||
@open="handleOpen"
|
||||
>
|
||||
<div class="article-editor">
|
||||
<div class="article-editor__left">
|
||||
<div class="article-editor__form">
|
||||
<el-form :model="form" label-width="76px" size="small">
|
||||
<el-form-item label="文章标题">
|
||||
<el-input v-model.trim="form.title" placeholder="请输入文章标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="文章摘要">
|
||||
<el-input
|
||||
v-model.trim="form.summary"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="120"
|
||||
show-word-limit
|
||||
placeholder="请输入文章摘要"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="article-editor__inline">
|
||||
<el-form-item label="文章类型">
|
||||
<el-select v-model="form.type" placeholder="请选择文章类型">
|
||||
<el-option label="企业动态" value="企业动态" />
|
||||
<el-option label="产品动态" value="产品动态" />
|
||||
<el-option label="行业洞察" value="行业洞察" />
|
||||
<el-option label="活动资讯" value="活动资讯" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="发布时间">
|
||||
<el-date-picker
|
||||
v-model="form.publishTime"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="选择发布时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="封面配图">
|
||||
<div class="cover-row">
|
||||
<el-button type="primary" size="small" @click="$refs.coverInput.click()">上传图片</el-button>
|
||||
<el-input
|
||||
v-model.trim="form.coverUrl"
|
||||
class="cover-url-input"
|
||||
placeholder="可填写图片地址,或点击左侧上传本地图片"
|
||||
@input="handleCoverUrlInput"
|
||||
/>
|
||||
<span v-if="form.coverName" class="cover-name">{{ form.coverName }}</span>
|
||||
<input ref="coverInput" type="file" accept="image/*" class="hidden-input" @change="handleCoverUpload">
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="dialogVisible" class="wang-editor-wrap">
|
||||
<Toolbar
|
||||
class="wang-toolbar"
|
||||
:editor="editor"
|
||||
:default-config="toolbarConfig"
|
||||
:mode="editorMode"
|
||||
/>
|
||||
<Editor
|
||||
v-model="form.content"
|
||||
class="wang-editor"
|
||||
:default-config="editorConfig"
|
||||
:mode="editorMode"
|
||||
@onCreated="handleEditorCreated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="resize-handle"
|
||||
title="拖动调整预览宽度"
|
||||
@mousedown="startResize"
|
||||
></div>
|
||||
|
||||
<div class="article-editor__preview" :style="{ width: `${previewWidth}px` }">
|
||||
<div class="preview-head">
|
||||
<span>实时预览</span>
|
||||
<em>自动同步</em>
|
||||
</div>
|
||||
<div class="preview-card">
|
||||
<div v-if="coverImage" class="preview-cover">
|
||||
<img :src="coverImage" alt="cover">
|
||||
</div>
|
||||
<span class="preview-type">{{ form.type || '企业动态' }}</span>
|
||||
<h1>{{ form.title || '文章标题' }}</h1>
|
||||
<p class="preview-summary">{{ form.summary || '文章摘要预览...' }}</p>
|
||||
<p class="preview-date">{{ form.publishTime || today }}</p>
|
||||
<div class="preview-content" v-html="form.content || '文章内容预览...'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="article-editor-footer">
|
||||
<span class="editor-status">{{ dirty ? '未保存' : '已同步' }}</span>
|
||||
<div>
|
||||
<el-button size="small" @click="dialogVisible = false">取消</el-button>
|
||||
<el-button size="small" @click="handleSave('draft')">保存草稿</el-button>
|
||||
<el-button type="primary" size="small" @click="handleSave('published')">发布文章</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
|
||||
const normalizeImageUrl = (url) => {
|
||||
if (!url) return ''
|
||||
if (/^(https?:|blob:|data:|\/\/)/.test(url)) return url
|
||||
return `${window.location.origin}/idfile?path=${url}`
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'ArticleEditorDialog',
|
||||
components: { Editor, Toolbar },
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
article: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 标记当前表单是否有未保存改动。
|
||||
dirty: false,
|
||||
// 保存 wangEditor 实例,组件销毁时需要手动释放。
|
||||
editor: null,
|
||||
// wangEditor 模式配置。
|
||||
editorMode: 'default',
|
||||
// wangEditor 工具栏配置。
|
||||
toolbarConfig: {},
|
||||
// wangEditor 编辑器配置。
|
||||
editorConfig: {
|
||||
placeholder: '请输入文章内容...',
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
customUpload: (file, insertFn) => {
|
||||
// 正文图片参考商品上传做法:先保存 File,提交时随文章 FormData 一起传。
|
||||
this.form.contentFiles.push(file)
|
||||
// 本地生成预览地址,插入到编辑器内容中。
|
||||
const url = URL.createObjectURL(file)
|
||||
insertFn(url, file.name, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// 上传封面的本地预览地址,只用于页面显示,不提交给接口。
|
||||
coverPreview: '',
|
||||
// 右侧预览区域宽度。
|
||||
previewWidth: 400,
|
||||
// 标记预览区域是否正在拖拽调整宽度。
|
||||
resizing: false,
|
||||
// 记录拖拽开始时的鼠标位置。
|
||||
resizeStartX: 0,
|
||||
// 记录拖拽开始时的预览宽度。
|
||||
resizeStartWidth: 400,
|
||||
// 文章编辑表单数据。
|
||||
form: this.createEmptyForm()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dialogVisible: {
|
||||
get() {
|
||||
// 使用父组件传入的 visible 控制弹窗显示。
|
||||
return this.visible
|
||||
},
|
||||
set(value) {
|
||||
// 通过 sync 事件通知父组件更新 visible。
|
||||
this.$emit('update:visible', value)
|
||||
}
|
||||
},
|
||||
dialogTitle() {
|
||||
// 有文章数据时是编辑,否则是新建。
|
||||
return this.article ? '编辑文章' : '新建文章'
|
||||
},
|
||||
today() {
|
||||
// 生成默认发布时间,格式与接口要求一致。
|
||||
const date = new Date()
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${date.getDate()}`.padStart(2, '0')
|
||||
return `${date.getFullYear()}-${month}-${day}`
|
||||
},
|
||||
coverImage() {
|
||||
// 优先使用本地预览,没有预览时使用接口/手动输入的图片路径。
|
||||
if (this.coverPreview) return this.coverPreview
|
||||
return normalizeImageUrl(this.form.coverUrl)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
form: {
|
||||
deep: true,
|
||||
handler() {
|
||||
// 任意表单字段变化后标记为未保存。
|
||||
this.dirty = true
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 组件销毁前释放 wangEditor 实例,避免内存泄漏。
|
||||
if (this.editor) {
|
||||
this.editor.destroy()
|
||||
this.editor = null
|
||||
}
|
||||
// 清理拖拽监听事件。
|
||||
this.removeResizeListeners()
|
||||
},
|
||||
methods: {
|
||||
createEmptyForm() {
|
||||
// 创建新增文章时使用的默认表单结构。
|
||||
return {
|
||||
id: '',
|
||||
title: '',
|
||||
summary: '',
|
||||
type: '企业动态',
|
||||
status: '0',
|
||||
publishTime: '',
|
||||
views: 0,
|
||||
content: '',
|
||||
contentFiles: [],
|
||||
coverUrl: '',
|
||||
coverFile: null,
|
||||
coverName: ''
|
||||
}
|
||||
},
|
||||
handleOpen() {
|
||||
// 弹窗打开时合并默认值和待编辑文章数据。
|
||||
this.form = {
|
||||
...this.createEmptyForm(),
|
||||
...(this.article || {})
|
||||
}
|
||||
// 没有发布时间时默认使用当天日期。
|
||||
if (!this.form.publishTime) this.form.publishTime = this.today
|
||||
// 初始化封面预览,新上传前优先显示接口返回的封面地址。
|
||||
this.coverPreview = ''
|
||||
this.$nextTick(() => {
|
||||
// 表单初始化完成后重置未保存状态。
|
||||
this.dirty = false
|
||||
})
|
||||
},
|
||||
handleEditorCreated(editor) {
|
||||
// 保存编辑器实例,后续销毁时使用。
|
||||
this.editor = Object.seal(editor)
|
||||
},
|
||||
handleCoverUpload(event) {
|
||||
// 读取用户选择的封面文件。
|
||||
const file = event.target.files && event.target.files[0]
|
||||
if (!file) return
|
||||
// 保存文件名用于页面提示。
|
||||
this.form.coverName = file.name
|
||||
// 保存原始文件,父组件提交时会放入 FormData 的 cover_img。
|
||||
this.form.coverFile = file
|
||||
// 生成本地预览地址,注意这个地址只用于预览,不提交给接口。
|
||||
this.coverPreview = URL.createObjectURL(file)
|
||||
// 清空 input 值,保证重复选择同一文件也能触发 change。
|
||||
event.target.value = ''
|
||||
},
|
||||
handleCoverUrlInput() {
|
||||
// 用户手动输入图片路径时,清空本地上传预览状态。
|
||||
this.coverPreview = ''
|
||||
this.form.coverFile = null
|
||||
this.form.coverName = ''
|
||||
},
|
||||
startResize(event) {
|
||||
// 开始拖拽调整右侧预览区域宽度。
|
||||
this.resizing = true
|
||||
// 记录拖拽起点。
|
||||
this.resizeStartX = event.clientX
|
||||
// 记录拖拽开始时的宽度。
|
||||
this.resizeStartWidth = this.previewWidth
|
||||
// 修改全局鼠标样式,让拖拽反馈更明显。
|
||||
document.body.style.cursor = 'col-resize'
|
||||
// 禁止选中文本,避免拖拽时误选页面文字。
|
||||
document.body.style.userSelect = 'none'
|
||||
// 监听鼠标移动和松开事件。
|
||||
window.addEventListener('mousemove', this.handleResize)
|
||||
window.addEventListener('mouseup', this.stopResize)
|
||||
},
|
||||
handleResize(event) {
|
||||
// 非拖拽状态下不处理移动事件。
|
||||
if (!this.resizing) return
|
||||
// 根据鼠标偏移计算新的预览宽度。
|
||||
const delta = this.resizeStartX - event.clientX
|
||||
const nextWidth = this.resizeStartWidth + delta
|
||||
// 限制预览宽度范围,避免过窄或过宽。
|
||||
this.previewWidth = Math.min(Math.max(nextWidth, 320), 720)
|
||||
},
|
||||
stopResize() {
|
||||
// 结束拖拽状态。
|
||||
this.resizing = false
|
||||
// 恢复全局鼠标样式。
|
||||
document.body.style.cursor = ''
|
||||
// 恢复页面文本选择。
|
||||
document.body.style.userSelect = ''
|
||||
// 移除拖拽事件监听。
|
||||
this.removeResizeListeners()
|
||||
},
|
||||
removeResizeListeners() {
|
||||
// 移除鼠标移动监听。
|
||||
window.removeEventListener('mousemove', this.handleResize)
|
||||
// 移除鼠标松开监听。
|
||||
window.removeEventListener('mouseup', this.stopResize)
|
||||
},
|
||||
handleSave(status) {
|
||||
// 第一步:校验文章标题。
|
||||
if (!this.form.title) {
|
||||
this.$message.warning('请输入文章标题')
|
||||
return
|
||||
}
|
||||
// 第二步:校验文章摘要。
|
||||
if (!this.form.summary) {
|
||||
this.$message.warning('请输入文章摘要')
|
||||
return
|
||||
}
|
||||
// 第三步:校验发布时间,确保提交接口时有 publish_time。
|
||||
if (!this.form.publishTime) {
|
||||
this.$message.warning('请选择发布时间')
|
||||
return
|
||||
}
|
||||
// 第四步:校验封面图片,确保提交接口时有 cover_img。
|
||||
if (!this.form.coverFile && !this.form.coverUrl) {
|
||||
this.$message.warning('请上传封面图片或填写图片地址')
|
||||
return
|
||||
}
|
||||
// 第五步:校验文章正文。
|
||||
if (!this.form.content) {
|
||||
this.$message.warning('请输入文章正文')
|
||||
return
|
||||
}
|
||||
// 第六步:组装保存参数,status 由按钮决定是草稿还是发布。
|
||||
const payload = {
|
||||
...this.form,
|
||||
publish_time: this.form.publishTime,
|
||||
status
|
||||
}
|
||||
// 第七步:把保存参数交给父组件,由父组件调用新增或编辑接口。
|
||||
this.$emit('save', payload)
|
||||
// 第八步:保存触发后重置未保存状态。
|
||||
this.dirty = false
|
||||
// 第九步:关闭弹窗。
|
||||
this.dialogVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
::v-deep .article-editor-dialog {
|
||||
margin: 0 !important;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
::v-deep .article-editor-dialog .el-dialog__header {
|
||||
flex-shrink: 0;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
::v-deep .article-editor-dialog .el-dialog__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 0 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .article-editor-dialog .el-dialog__footer {
|
||||
flex-shrink: 0;
|
||||
padding: 14px 24px;
|
||||
border-top: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.article-editor {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.article-editor__left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: relative;
|
||||
flex: 0 0 10px;
|
||||
width: 10px;
|
||||
cursor: col-resize;
|
||||
background: #f8fafc;
|
||||
border-left: 1px solid #edf0f5;
|
||||
border-right: 1px solid #edf0f5;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.resize-handle::before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 3px;
|
||||
height: 42px;
|
||||
content: '';
|
||||
border-radius: 999px;
|
||||
background: #cbd5e1;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: background 0.2s ease, height 0.2s ease;
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.resize-handle:hover::before {
|
||||
height: 58px;
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.article-editor__form {
|
||||
padding: 16px 18px 4px 0;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.article-editor__inline {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.article-editor__inline .el-form-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cover-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cover-url-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cover-name {
|
||||
color: #16a34a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.hidden-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.article-editor__toolbar-tip {
|
||||
padding: 10px 18px 10px 0;
|
||||
color: #8a94a6;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wang-editor-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding-right: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wang-toolbar {
|
||||
border: 1px solid #edf0f5;
|
||||
border-bottom: 0;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
.wang-editor {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 0 0 10px 10px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.article-editor__preview {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.preview-head {
|
||||
height: 48px;
|
||||
padding: 0 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.preview-head span {
|
||||
color: #344054;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.preview-head em {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin: 18px;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.preview-cover {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.preview-cover img {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.preview-type {
|
||||
display: inline-flex;
|
||||
padding: 2px 8px;
|
||||
margin-bottom: 10px;
|
||||
color: #2563eb;
|
||||
font-size: 12px;
|
||||
background: #eff6ff;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.preview-card h1 {
|
||||
margin: 0 0 8px;
|
||||
color: #1f2937;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.preview-date {
|
||||
margin: 0 0 18px;
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.preview-summary {
|
||||
margin: 0 0 10px;
|
||||
color: #667085;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
color: #4b5563;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.article-editor-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.editor-status {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
719
f/web-kboss/src/views/homePage/news/newsLog.vue
Normal file
719
f/web-kboss/src/views/homePage/news/newsLog.vue
Normal file
@ -0,0 +1,719 @@
|
||||
<template>
|
||||
<div class="article-admin-page">
|
||||
<div class="page-title-wrap">
|
||||
<h1>企业文章</h1>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div
|
||||
v-for="item in statsCards"
|
||||
:key="item.type"
|
||||
class="stats-card"
|
||||
>
|
||||
<div class="stats-icon" :class="item.iconClass">
|
||||
<i :class="item.icon"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stats-label">{{ item.label }}</div>
|
||||
<div class="stats-value">{{ item.count }} 篇</div>
|
||||
<div class="stats-read">阅读 {{ item.views }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-card">
|
||||
<el-form :model="queryForm" inline class="filter-form">
|
||||
<el-form-item label="文章类型">
|
||||
<el-select v-model="queryForm.type" size="small" clearable placeholder="全部类型" @change="handleSearch">
|
||||
<el-option label="企业动态" value="企业动态" />
|
||||
<el-option label="产品动态" value="产品动态" />
|
||||
<el-option label="行业洞察" value="行业洞察" />
|
||||
<el-option label="活动资讯" value="活动资讯" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryForm.status" size="small" clearable placeholder="全部状态" @change="handleSearch">
|
||||
<el-option label="已发布" value="published" />
|
||||
<el-option label="草稿" value="draft" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model.trim="queryForm.keyword"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="搜索文章标题..."
|
||||
@keyup.enter.native="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" size="small" icon="el-icon-search" @click="handleSearch">搜索</el-button>
|
||||
<el-button type="primary" size="small" icon="el-icon-plus" @click="openArticleEditor">新建文章</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<el-table
|
||||
:data="pagedArticles"
|
||||
v-loading="loading"
|
||||
class="article-table"
|
||||
style="width: 100%"
|
||||
empty-text="暂无文章,点击“新建文章”开始创作"
|
||||
>
|
||||
<el-table-column label="序号" width="80">
|
||||
<template slot-scope="scope">
|
||||
{{ getRowNo(scope.$index) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="标题" min-width="260" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span class="article-title">{{ scope.row.title }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="120">
|
||||
<template slot-scope="scope">
|
||||
<span class="type-tag" :class="getTypeClass(scope.row.type)">{{ scope.row.type }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="110">
|
||||
<template slot-scope="scope">
|
||||
<span class="status-tag" :class="scope.row.status === 'published' ? 'is-published' : 'is-draft'">
|
||||
<i></i>{{ scope.row.status === 'published' ? '已发布' : '草稿' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="publishTime" label="发布时间" width="140" />
|
||||
<el-table-column label="阅读数据" width="130">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.status === 'published'" class="views-text">
|
||||
<i class="el-icon-view"></i>{{ scope.row.views }}
|
||||
</span>
|
||||
<span v-else class="empty-read">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
size="mini"
|
||||
:disabled="scope.row.status === 'published'"
|
||||
@click="editArticle(scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'published'"
|
||||
type="text"
|
||||
size="mini"
|
||||
class="warning-text"
|
||||
@click="unpublishArticle(scope.row)"
|
||||
>
|
||||
下架
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="text"
|
||||
size="mini"
|
||||
class="success-text"
|
||||
@click="publishArticle(scope.row)"
|
||||
>
|
||||
上架
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
size="mini"
|
||||
class="danger-text"
|
||||
:disabled="scope.row.status === 'published'"
|
||||
@click="deleteArticle(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table-pagination">
|
||||
<span class="pagination-info">显示 {{ pageStart }}-{{ pageEnd }} 条,共 {{ total }} 条</span>
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next, jumper"
|
||||
:current-page.sync="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article-editor-dialog
|
||||
:visible.sync="editorVisible"
|
||||
:article="currentArticle"
|
||||
@save="handleSaveArticle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ArticleEditorDialog from './ArticleEditorDialog.vue'
|
||||
import {
|
||||
reqAddNews,
|
||||
reqDeleteNews,
|
||||
reqEditNews,
|
||||
reqNewsListAdmin,
|
||||
reqPublishNews,
|
||||
reqUnpublishNews
|
||||
} from '@/api/newsapi/newsapi'
|
||||
export default {
|
||||
name: 'NewsLog',
|
||||
components: { ArticleEditorDialog },
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
loading: false,
|
||||
editorVisible: false,
|
||||
currentArticle: null,
|
||||
queryForm: {
|
||||
type: '',
|
||||
status: '',
|
||||
keyword: ''
|
||||
},
|
||||
articleList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredArticles() {
|
||||
return this.articleList
|
||||
},
|
||||
pagedArticles() {
|
||||
return this.articleList
|
||||
},
|
||||
pageStart() {
|
||||
if (!this.total) return 0
|
||||
return (this.page - 1) * this.pageSize + 1
|
||||
},
|
||||
pageEnd() {
|
||||
if (!this.total) return 0
|
||||
return Math.min(this.page * this.pageSize, this.total)
|
||||
},
|
||||
statsCards() {
|
||||
const map = {
|
||||
企业动态: { label: '企业动态', type: '企业动态', icon: 'el-icon-document', iconClass: 'blue' },
|
||||
产品动态: { label: '产品动态', type: '产品动态', icon: 'el-icon-box', iconClass: 'green' },
|
||||
行业洞察: { label: '行业洞察', type: '行业洞察', icon: 'el-icon-data-analysis', iconClass: 'amber' },
|
||||
活动资讯: { label: '活动资讯', type: '活动资讯', icon: 'el-icon-date', iconClass: 'purple' }
|
||||
}
|
||||
return Object.values(map).map(item => {
|
||||
const list = this.articleList.filter(article => article.type === item.type)
|
||||
return {
|
||||
...item,
|
||||
count: list.length,
|
||||
views: list.reduce((total, article) => total + Number(article.views || 0), 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 页面创建后立即拉取第一页文章列表。
|
||||
this.getArticleList()
|
||||
},
|
||||
methods: {
|
||||
getRowNo(index) {
|
||||
// 根据当前分页计算表格序号。
|
||||
return (this.page - 1) * this.pageSize + index + 1
|
||||
},
|
||||
getTypeClass(type) {
|
||||
// 按文章类型返回对应的标签样式类。
|
||||
const classMap = {
|
||||
企业动态: 'type-blue',
|
||||
产品动态: 'type-purple',
|
||||
行业洞察: 'type-amber',
|
||||
活动资讯: 'type-violet'
|
||||
}
|
||||
return classMap[type] || 'type-blue'
|
||||
},
|
||||
handleSearch() {
|
||||
// 筛选条件变化后从第一页重新查询。
|
||||
this.page = 1
|
||||
this.getArticleList()
|
||||
},
|
||||
handlePageChange(page) {
|
||||
// 用户切换分页后更新页码并重新拉取数据。
|
||||
this.page = page
|
||||
this.getArticleList()
|
||||
},
|
||||
openArticleEditor() {
|
||||
// 清空当前文章,打开新建弹窗。
|
||||
this.currentArticle = null
|
||||
this.editorVisible = true
|
||||
},
|
||||
editArticle(row) {
|
||||
// 编辑回显直接读取当前表格行数据,不再请求详情接口。
|
||||
this.currentArticle = { ...row }
|
||||
this.editorVisible = true
|
||||
},
|
||||
getApiStatus(status) {
|
||||
// 页面状态转成接口需要的状态值:1 上架,0 草稿/下架。
|
||||
return status === 'published' ? '1' : '0'
|
||||
},
|
||||
getViewStatus(status) {
|
||||
// 接口状态转成页面使用的状态枚举。
|
||||
return String(status) === '1' || status === 'published' ? 'published' : 'draft'
|
||||
},
|
||||
isSuccess(res) {
|
||||
// 兼容不同接口的成功标识。
|
||||
return res && (res.status === true || res.status === 'true' || res.code === 200)
|
||||
},
|
||||
buildQueryParams() {
|
||||
// 组装后台列表查询参数。
|
||||
return {
|
||||
url_link: window.location.href,
|
||||
current_page: String(this.page),
|
||||
page_size: String(this.pageSize),
|
||||
title: this.queryForm.keyword.trim(),
|
||||
article_type: this.queryForm.type,
|
||||
status: this.queryForm.status ? this.getApiStatus(this.queryForm.status) : ''
|
||||
}
|
||||
},
|
||||
normalizeArticle(row) {
|
||||
// 将接口字段统一转换成页面表格和编辑弹窗使用的字段。
|
||||
return {
|
||||
id: row.id,
|
||||
title: row.title || '',
|
||||
summary: row.summary || '',
|
||||
type: row.article_type || row.type || '企业动态',
|
||||
status: this.getViewStatus(row.status),
|
||||
publishTime: row.publish_time || row.publishTime || '',
|
||||
views: row.read_count || row.views || 0,
|
||||
content: row.content || '',
|
||||
coverUrl: row.cover_img || row.coverUrl || '',
|
||||
coverName: row.cover_name || row.coverName || ''
|
||||
}
|
||||
},
|
||||
getResponseList(res) {
|
||||
// 兼容接口可能返回的多种列表结构。
|
||||
if (Array.isArray(res.data)) return res.data
|
||||
if (res.data && Array.isArray(res.data.data)) return res.data.data
|
||||
if (res.data && Array.isArray(res.data.list)) return res.data.list
|
||||
if (Array.isArray(res.list)) return res.list
|
||||
return []
|
||||
},
|
||||
getResponseTotal(res, list) {
|
||||
// 兼容接口可能返回的多种总数字段。
|
||||
const data = res.data && !Array.isArray(res.data) ? res.data : {}
|
||||
const pagination = res.pagination || data.pagination || data.page || {}
|
||||
return Number(
|
||||
res.total ||
|
||||
res.total_count ||
|
||||
data.total ||
|
||||
data.total_count ||
|
||||
pagination.total ||
|
||||
list.length
|
||||
)
|
||||
},
|
||||
buildArticlePayload(article) {
|
||||
// 将编辑弹窗字段转换成新增/编辑接口需要的字段。
|
||||
const payload = {
|
||||
...(article.id ? { id: article.id } : {}),
|
||||
url_link: window.location.href,
|
||||
title: article.title,
|
||||
article_type: article.type,
|
||||
summary: article.summary,
|
||||
publish_time: article.publish_time || article.publishTime,
|
||||
content: article.content,
|
||||
status: this.getApiStatus(article.status)
|
||||
}
|
||||
const hasContentFiles = Array.isArray(article.contentFiles) && article.contentFiles.length > 0
|
||||
if (article.coverFile || hasContentFiles) {
|
||||
const formData = new FormData()
|
||||
Object.keys(payload).forEach(key => {
|
||||
formData.append(key, payload[key])
|
||||
})
|
||||
if (article.coverFile) {
|
||||
formData.append('cover_img', article.coverFile)
|
||||
} else {
|
||||
formData.append('cover_img', article.coverUrl)
|
||||
}
|
||||
if (hasContentFiles) {
|
||||
article.contentFiles.forEach(file => {
|
||||
formData.append('content_images', file)
|
||||
})
|
||||
}
|
||||
return formData
|
||||
}
|
||||
return {
|
||||
...payload,
|
||||
cover_img: article.coverUrl
|
||||
}
|
||||
},
|
||||
async confirmAction(title, message, type = 'warning') {
|
||||
// 操作前弹出二次确认,用户取消时直接终止后续请求。
|
||||
try {
|
||||
await this.$confirm(message, title, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type
|
||||
})
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
async getArticleList() {
|
||||
// 开启表格 loading,防止用户误以为页面没有响应。
|
||||
this.loading = true
|
||||
try {
|
||||
// 按当前筛选条件和分页请求后台文章列表。
|
||||
const res = await reqNewsListAdmin(this.buildQueryParams())
|
||||
if (this.isSuccess(res)) {
|
||||
// 请求成功后解析列表数据。
|
||||
const list = this.getResponseList(res)
|
||||
// 将接口数据映射成页面展示字段。
|
||||
this.articleList = list.map(item => this.normalizeArticle(item))
|
||||
// 读取接口总数,用于分页器和底部统计。
|
||||
this.total = this.getResponseTotal(res, list)
|
||||
return
|
||||
}
|
||||
// 接口返回失败时清空列表,避免展示旧数据。
|
||||
this.articleList = []
|
||||
this.total = 0
|
||||
} catch (error) {
|
||||
// 请求异常时清空列表并提示用户。
|
||||
this.articleList = []
|
||||
this.total = 0
|
||||
this.$message.error('文章列表加载失败')
|
||||
} finally {
|
||||
// 无论成功失败都关闭 loading。
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async handleSaveArticle(article) {
|
||||
// 根据是否存在 id 判断是编辑还是新增。
|
||||
const request = article.id ? reqEditNews : reqAddNews
|
||||
try {
|
||||
// 调用新增或编辑接口保存文章。
|
||||
const res = await request(this.buildArticlePayload(article))
|
||||
if (!this.isSuccess(res)) {
|
||||
// 接口明确返回失败时展示后端错误信息。
|
||||
this.$message.error((res && (res.message || res.msg)) || '保存失败')
|
||||
return
|
||||
}
|
||||
// 保存成功后提示,并刷新列表展示最新数据。
|
||||
this.$message.success(article.status === 'published' ? '发布成功' : '草稿已保存')
|
||||
this.getArticleList()
|
||||
} catch (error) {
|
||||
// 网络或程序异常时提示保存失败。
|
||||
this.$message.error('保存失败')
|
||||
}
|
||||
},
|
||||
async publishArticle(row) {
|
||||
// 上架前先二次确认,避免误操作。
|
||||
const confirmed = await this.confirmAction('确认上架', `确定要上架「${row.title}」吗?`)
|
||||
if (!confirmed) return
|
||||
try {
|
||||
// 调用上架接口。
|
||||
const res = await reqPublishNews({ id: row.id })
|
||||
if (!this.isSuccess(res)) {
|
||||
// 接口返回失败时展示错误并停止刷新。
|
||||
this.$message.error((res && (res.message || res.msg)) || '发布失败')
|
||||
return
|
||||
}
|
||||
// 上架成功后刷新列表。
|
||||
this.$message.success('上架成功')
|
||||
this.getArticleList()
|
||||
} catch (error) {
|
||||
// 请求异常时提示用户。
|
||||
this.$message.error('上架失败')
|
||||
}
|
||||
},
|
||||
async unpublishArticle(row) {
|
||||
// 下架前先二次确认,避免误操作。
|
||||
const confirmed = await this.confirmAction('确认下架', `确定要下架「${row.title}」吗?`)
|
||||
if (!confirmed) return
|
||||
try {
|
||||
// 调用下架接口。
|
||||
const res = await reqUnpublishNews({ id: row.id })
|
||||
if (!this.isSuccess(res)) {
|
||||
// 接口返回失败时展示错误并停止刷新。
|
||||
this.$message.error((res && (res.message || res.msg)) || '下架失败')
|
||||
return
|
||||
}
|
||||
// 下架成功后刷新列表。
|
||||
this.$message.success('下架成功')
|
||||
this.getArticleList()
|
||||
} catch (error) {
|
||||
// 请求异常时提示用户。
|
||||
this.$message.error('下架失败')
|
||||
}
|
||||
},
|
||||
async deleteArticle(row) {
|
||||
// 删除前先二次确认,避免误删文章。
|
||||
const confirmed = await this.confirmAction('确认删除', `删除后不可恢复,确定要删除「${row.title}」吗?`, 'error')
|
||||
if (!confirmed) return
|
||||
try {
|
||||
// 调用删除接口。
|
||||
const res = await reqDeleteNews({ id: row.id })
|
||||
if (!this.isSuccess(res)) {
|
||||
// 接口返回失败时展示错误并停止刷新。
|
||||
this.$message.error((res && (res.message || res.msg)) || '删除失败')
|
||||
return
|
||||
}
|
||||
// 删除当前页最后一条时,自动回到上一页。
|
||||
if (this.articleList.length === 1 && this.page > 1) this.page -= 1
|
||||
// 删除成功后刷新列表。
|
||||
this.$message.success('删除成功')
|
||||
this.getArticleList()
|
||||
} catch (error) {
|
||||
// 请求异常时提示用户。
|
||||
this.$message.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.article-admin-page {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 24px;
|
||||
background: #f3f7ff;
|
||||
}
|
||||
|
||||
.page-title-wrap {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title-wrap h1 {
|
||||
margin: 0;
|
||||
color: #1f2937;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stats-icon.blue {
|
||||
color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.stats-icon.green {
|
||||
color: #10b981;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.stats-icon.amber {
|
||||
color: #f59e0b;
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.stats-icon.purple {
|
||||
color: #8b5cf6;
|
||||
background: #f5f3ff;
|
||||
}
|
||||
|
||||
.stats-label,
|
||||
.stats-read {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
margin: 3px 0;
|
||||
color: #1f2937;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.filter-card,
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-form ::v-deep .el-form-item {
|
||||
margin-right: 14px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-form ::v-deep .el-input,
|
||||
.filter-form ::v-deep .el-select {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
.filter-form ::v-deep .el-input__inner {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.article-table ::v-deep th {
|
||||
background: #f8fafc;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
color: #1f2937;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.type-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.type-blue {
|
||||
color: #2563eb;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.type-purple {
|
||||
color: #7c3aed;
|
||||
background: #f5f3ff;
|
||||
}
|
||||
|
||||
.type-amber {
|
||||
color: #d97706;
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.type-violet {
|
||||
color: #8b5cf6;
|
||||
background: #f5f3ff;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-tag i {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-tag.is-published {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.status-tag.is-published i {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.status-tag.is-draft {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.status-tag.is-draft i {
|
||||
background: #d1d5db;
|
||||
}
|
||||
|
||||
.views-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty-read {
|
||||
color: #d1d5db;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.success-text {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.danger-text {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.article-admin-page {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -28,7 +28,7 @@
|
||||
type="button"
|
||||
class="filter-tab"
|
||||
:class="{ active: activeCategory === item.value }"
|
||||
@click="activeCategory = item.value"
|
||||
@click="handleCategoryChange(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
@ -83,11 +83,22 @@
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<button type="button" class="pagination-btn"><i class="el-icon-arrow-left"></i></button>
|
||||
<button type="button" class="pagination-btn active">1</button>
|
||||
<button type="button" class="pagination-btn">2</button>
|
||||
<button type="button" class="pagination-btn">3</button>
|
||||
<button type="button" class="pagination-btn"><i class="el-icon-arrow-right"></i></button>
|
||||
<button type="button" class="pagination-btn" :disabled="page <= 1" @click="changePage(page - 1)">
|
||||
<i class="el-icon-arrow-left"></i>
|
||||
</button>
|
||||
<button
|
||||
v-for="item in visiblePages"
|
||||
:key="item"
|
||||
type="button"
|
||||
class="pagination-btn"
|
||||
:class="{ active: page === item }"
|
||||
@click="changePage(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</button>
|
||||
<button type="button" class="pagination-btn" :disabled="page >= totalPages" @click="changePage(page + 1)">
|
||||
<i class="el-icon-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -95,137 +106,112 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reqNewsList } from '@/api/newsapi/newsapi'
|
||||
export default {
|
||||
name: 'NewsView',
|
||||
data() {
|
||||
return {
|
||||
fallbackNewsImage: require('@/assets/image/news.jpg'),
|
||||
activeCategory: 'all',
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
filterTabs: [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '企业动态', value: 'company' },
|
||||
{ label: '产品动态', value: 'product' },
|
||||
{ label: '行业洞察', value: 'industry' },
|
||||
{ label: '活动资讯', value: 'event' }
|
||||
{ label: '企业动态', value: '企业动态' },
|
||||
{ label: '产品动态', value: '产品动态' },
|
||||
{ label: '行业洞察', value: '行业洞察' },
|
||||
{ label: '活动资讯', value: '活动资讯' }
|
||||
],
|
||||
newsList: [
|
||||
{
|
||||
id: 0,
|
||||
category: 'company',
|
||||
tag: '企业动态',
|
||||
tagColor: 'rgba(16,185,129,0.85)',
|
||||
date: '2026.6.15',
|
||||
title: '开元云随贸促会走访东盟',
|
||||
desc: '开元云科技随贸促会广西分会经贸代表团密集出访越南、老挝,深度参与中国-东盟经贸合作,以AI技术赋能区域产业升级,推动智能体工厂落地东南亚市场。',
|
||||
img: '',
|
||||
featured: true,
|
||||
content: '<p>开元云科技随贸促会广西分会经贸代表团密集出访越南、老挝,深度参与中国-东盟经贸合作。</p><p>在此次出访中,开元云向东南亚市场全面展示了AI智能体工厂的工业级交付能力,涵盖智慧工程、智能运维、能源管理等多个核心领域的技术方案。</p><p>此次东盟之行标志着开元云正式开启东南亚市场战略布局,未来将持续深化与东盟各国在AI领域的合作,以技术赋能区域产业升级。</p>'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'company',
|
||||
tag: '企业动态',
|
||||
tagColor: 'rgba(16,185,129,0.85)',
|
||||
date: '2026.05.17',
|
||||
title: '开元云荣登福布斯中国人工智能商业落地示范企业',
|
||||
desc: '凭借AI智能体工厂的工业级交付能力与央国企标杆案例,开元云入选福布斯中国人工智能商业落地示范企业,成为行业AI落地标杆。',
|
||||
content: '<p>凭借AI智能体工厂的工业级交付能力与央国企标杆案例,开元云入选福布斯中国人工智能商业落地示范企业。</p><p>此次评选从技术实力、商业落地、行业影响力等多维度综合评估,开元云凭借智能体工厂产品创新力成功跻身示范企业榜单。</p>'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: 'product',
|
||||
tag: '产品动态',
|
||||
tagColor: 'rgba(99,102,241,0.85)',
|
||||
date: '2026.05.10',
|
||||
title: '智能体工厂2.0正式发布,全面升级AI交付能力',
|
||||
desc: '开元云智能体工厂2.0版本重磅上线,新增多智能体协同编排、可视化工作流设计等核心功能,AI应用交付效率提升300%。',
|
||||
content: '<p>开元云智能体工厂2.0版本重磅上线,新增多智能体协同编排、可视化工作流设计等核心功能。</p><p>2.0版本支持复杂业务场景的智能体协作、拖拽式工作流构建和增强的文档智能处理能力。</p>'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category: 'industry',
|
||||
tag: '行业洞察',
|
||||
tagColor: 'rgba(13,148,136,0.85)',
|
||||
date: '2026.04.28',
|
||||
title: '2026年AI+能源行业趋势:从预测性维护到智能调度',
|
||||
desc: 'AI技术正在重塑能源行业,从发电设备的预测性维护到电网智能调度,开元云深度解析行业变革趋势与落地实践。',
|
||||
content: '<p>AI技术正在重塑能源行业,从发电设备的预测性维护到电网智能调度,行业正在进入智能化深水区。</p><p>基于机器视觉和传感器数据融合的预测性维护方案已在火电、风电领域广泛应用。</p>'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
category: 'event',
|
||||
tag: '活动资讯',
|
||||
tagColor: 'rgba(245,158,11,0.85)',
|
||||
date: '2026.04.15',
|
||||
title: '开元云亮相2026中国人工智能大会',
|
||||
desc: '开元云受邀出席2026中国人工智能大会,现场展示智能体工厂最新技术成果,与行业领袖共话AI产业化新路径。',
|
||||
content: '<p>开元云受邀出席2026中国人工智能大会,现场展示智能体工厂最新技术成果。</p><p>在大会主论坛上,开元云系统阐述了AI技术从实验创新到工业级交付的方法论。</p>'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
category: 'company',
|
||||
tag: '企业动态',
|
||||
tagColor: 'rgba(16,185,129,0.85)',
|
||||
date: '2026.03.22',
|
||||
title: '开元云与南宁林业局达成智慧林业战略合作',
|
||||
desc: '开元云携手南宁林业局打造林业空间采伐智审系统,融合GIS空间分析、卫星遥感等多源数据,审批周期从数天压缩至分钟级。',
|
||||
img: '',
|
||||
content: '<p>开元云携手南宁林业局打造林业空间采伐智审系统,融合GIS空间分析、卫星遥感等多源数据。</p><p>项目上线后,采伐审批效率显著提升,成为智慧林业标杆案例。</p>'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
category: 'product',
|
||||
tag: '产品动态',
|
||||
tagColor: 'rgba(236,72,153,0.85)',
|
||||
date: '2026.03.08',
|
||||
title: '机器视觉检测平台升级,支持工业质检全场景覆盖',
|
||||
desc: '开元云机器视觉检测平台全面升级,新增缺陷分类、尺寸测量、表面检测三大能力模块,覆盖制造业全场景质检需求。',
|
||||
content: '<p>开元云机器视觉检测平台全面升级,新增缺陷分类、尺寸测量、表面检测三大能力模块。</p><p>该平台已在汽车零部件、3C电子、半导体封装等行业完成部署。</p>'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
category: 'industry',
|
||||
tag: '行业洞察',
|
||||
tagColor: 'rgba(37,99,235,0.85)',
|
||||
date: '2026.02.20',
|
||||
title: 'AI+教育:大模型如何重构个性化学习路径',
|
||||
desc: '从知识图谱构建到学习路径智能推荐,大模型正在从根本上改变教育模式。开元云分享AI+教育的最新落地实践与思考。',
|
||||
content: '<p>从知识图谱构建到学习路径智能推荐,大模型正在从根本上改变教育模式。</p><p>基于大模型的自适应学习系统可以实时评估学习者知识掌握程度,动态调整学习路径。</p>'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
category: 'event',
|
||||
tag: '活动资讯',
|
||||
tagColor: 'rgba(124,58,237,0.85)',
|
||||
date: '2026.02.05',
|
||||
title: '开元云荣获2025年度AI创新企业TOP50',
|
||||
desc: '在2025年度人工智能创新企业评选中,开元云凭借卓越的技术创新能力和丰富的行业落地经验,成功入选AI创新企业TOP50。',
|
||||
content: '<p>在2025年度人工智能创新企业评选中,开元云凭借卓越的技术创新能力和丰富的行业落地经验成功入选。</p>'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
category: 'company',
|
||||
tag: '企业动态',
|
||||
tagColor: 'rgba(234,88,12,0.85)',
|
||||
date: '2026.01.18',
|
||||
title: '开元云完成B轮融资,加速AI智能体产业布局',
|
||||
desc: '开元云科技宣布完成B轮融资,融资金额将用于加速AI智能体工厂产品研发、行业解决方案深化及海外市场拓展。',
|
||||
content: '<p>开元云科技宣布完成B轮融资,融资金额将用于加速AI智能体工厂产品研发、行业解决方案深化及海外市场拓展。</p>'
|
||||
}
|
||||
]
|
||||
newsList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
featuredNews() {
|
||||
return this.newsList.find(item => item.featured)
|
||||
return this.newsList[0]
|
||||
},
|
||||
filteredNewsList() {
|
||||
return this.newsList.filter(item => !item.featured && (this.activeCategory === 'all' || item.category === this.activeCategory))
|
||||
return this.newsList.slice(1)
|
||||
},
|
||||
totalPages() {
|
||||
return Math.max(Math.ceil(this.total / this.pageSize), 1)
|
||||
},
|
||||
visiblePages() {
|
||||
return Array.from({ length: Math.min(this.totalPages, 3) }, (_, index) => index + 1)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getNewsList()
|
||||
},
|
||||
methods: {
|
||||
goAbout() {
|
||||
this.$router.push('/homePage/about')
|
||||
},
|
||||
handleCategoryChange(value) {
|
||||
this.activeCategory = value
|
||||
this.page = 1
|
||||
this.getNewsList()
|
||||
},
|
||||
changePage(page) {
|
||||
if (page < 1 || page > this.totalPages || page === this.page) return
|
||||
this.page = page
|
||||
this.getNewsList()
|
||||
},
|
||||
buildNewsParams() {
|
||||
return {
|
||||
url_link: window.location.href.split('#')[0] || window.location.href,
|
||||
current_page: String(this.page),
|
||||
page_size: String(this.pageSize),
|
||||
article_type: this.activeCategory === 'all' ? '' : this.activeCategory,
|
||||
title: ''
|
||||
}
|
||||
},
|
||||
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)'
|
||||
}
|
||||
return map[type] || 'rgba(99,102,241,0.85)'
|
||||
},
|
||||
normalizeImageUrl(url) {
|
||||
if (!url) return ''
|
||||
if (/^(https?:|blob:|data:|\/\/)/.test(url)) return url
|
||||
return `${window.location.origin}/idfile?path=${url}`
|
||||
},
|
||||
normalizeNewsItem(row) {
|
||||
const type = row.article_type || '企业动态'
|
||||
return {
|
||||
id: row.id,
|
||||
category: type,
|
||||
tag: type,
|
||||
tagColor: this.getTagColor(type),
|
||||
date: row.publish_time || '',
|
||||
title: row.title || '未命名文章',
|
||||
desc: row.summary || '',
|
||||
img: this.normalizeImageUrl(row.cover_img || ''),
|
||||
views: row.read_count || 0
|
||||
}
|
||||
},
|
||||
async getNewsList() {
|
||||
try {
|
||||
const res = await reqNewsList(this.buildNewsParams())
|
||||
if (res && res.status === true) {
|
||||
this.newsList = Array.isArray(res.data) ? res.data.map(item => this.normalizeNewsItem(item)) : []
|
||||
const pagination = res.pagination || {}
|
||||
this.total = Number(pagination.total || this.newsList.length)
|
||||
this.page = Number(pagination.current_page || this.page)
|
||||
this.pageSize = Number(pagination.page_size || this.pageSize)
|
||||
return
|
||||
}
|
||||
this.newsList = []
|
||||
this.total = 0
|
||||
} catch (error) {
|
||||
this.newsList = []
|
||||
this.total = 0
|
||||
this.$message.error('新闻列表加载失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user