From 09449c8a4b8556bcb47519f00cc10793f3bfc1f4 Mon Sep 17 00:00:00 2001 From: ping <1017253325@qq.com> Date: Wed, 29 Jul 2026 15:43:04 +0800 Subject: [PATCH 1/9] docs: design bilingual enterprise news APIs Co-authored-by: Cursor --- ...-07-29-bilingual-enterprise-news-design.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-29-bilingual-enterprise-news-design.md diff --git a/docs/superpowers/specs/2026-07-29-bilingual-enterprise-news-design.md b/docs/superpowers/specs/2026-07-29-bilingual-enterprise-news-design.md new file mode 100644 index 0000000..851e2a5 --- /dev/null +++ b/docs/superpowers/specs/2026-07-29-bilingual-enterprise-news-design.md @@ -0,0 +1,37 @@ +# 中英文企业动态接口设计 + +## 目标 + +企业文章支持运营同时录入中文和英文内容。接口直接使用 +`enterprise_news_article` 表中的中英文字段,不再读写旧字段 +`title`、`article_type`、`summary`、`content`。 + +## 字段 + +- 中文:`title_zh`、`article_type_zh`、`summary_zh`、`content_zh` +- 英文:`title_en`、`article_type_en`、`summary_en`、`content_en` +- 共用:`cover_img`、`status`、`publish_time`、`read_count` + +新增文章仅要求 `title_zh` 必填,其余中英文字段允许为空。 + +## 接口调整 + +- `news_article_add.dspy`:接收并写入全部中英文字段。 +- `news_article_update.dspy`:按传入字段更新中英文内容。 +- `news_article_search.dspy`:支持中英文标题和类型筛选,返回全部中英文字段;类型汇总分别使用中英文类型。 +- `news_article_detail.dspy`:返回全部中英文字段。 +- `front_news_search.dspy`:支持中英文标题和类型筛选,返回全部中英文字段。 +- `front_news_detail.dspy`:返回全部中英文字段,并保持阅读量加一。 + +发布、下架、删除接口不涉及语言字段,不调整。 + +## 兼容边界 + +旧字段已由数据库移除非空约束,本次接口不再维护旧字段。调用方需要改为传入和读取中英文新字段。 + +## 验证 + +- 新增时缺少 `title_zh` 返回校验错误。 +- 新增、更新后详情能完整返回中英文字段。 +- 后台和前端列表能按中英文标题、类型筛选。 +- 前端详情仍仅允许读取已发布文章,并正确增加阅读量。 From 2dbf1a859e85dd4fc9f9d24fb78c00c8ad6aa8a6 Mon Sep 17 00:00:00 2001 From: ping <1017253325@qq.com> Date: Wed, 29 Jul 2026 16:42:48 +0800 Subject: [PATCH 2/9] update --- b/news/enterprise_news_article.sql | 27 +++ b/news/front_news_detail.dspy | 4 +- b/news/front_news_search.dspy | 15 +- b/news/news_article_add.dspy | 21 +- b/news/news_article_search.dspy | 57 ++++-- b/news/news_article_update.dspy | 19 +- .../2026-07-29-bilingual-enterprise-news.md | 187 ++++++++++++++++++ 7 files changed, 284 insertions(+), 46 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-29-bilingual-enterprise-news.md diff --git a/b/news/enterprise_news_article.sql b/b/news/enterprise_news_article.sql index 04c9193..e6fea1f 100644 --- a/b/news/enterprise_news_article.sql +++ b/b/news/enterprise_news_article.sql @@ -1,3 +1,30 @@ +CREATE TABLE `enterprise_news_article` ( + `id` varchar(32) NOT NULL COMMENT '唯一标识符', + `domain_name` varchar(64) NOT NULL COMMENT '所属域名', + `title_zh` varchar(100) DEFAULT NULL COMMENT '中文标题', + `title_en` varchar(100) DEFAULT NULL COMMENT '英文标题', + `summary_zh` varchar(255) DEFAULT NULL COMMENT '中文摘要', + `summary_en` varchar(255) DEFAULT NULL COMMENT '英文摘要', + `article_type_zh` varchar(20) DEFAULT NULL COMMENT '中文文章类型', + `article_type_en` varchar(20) DEFAULT NULL COMMENT '英文文章类型', + `content_zh` text DEFAULT NULL COMMENT '中文正文', + `content_en` text DEFAULT NULL COMMENT '英文正文', + `title` varchar(100) DEFAULT NULL COMMENT '文章标题', + `article_type` varchar(20) DEFAULT NULL COMMENT '文章类型(企业动态/产品动态/行业洞察/活动资讯)', + `summary` varchar(255) DEFAULT NULL COMMENT '文章摘要', + `cover_img` varchar(255) DEFAULT NULL COMMENT '封面图片', + `content` text DEFAULT NULL COMMENT '文章正文', + `status` varchar(1) DEFAULT '0' COMMENT '状态(0-草稿/1-已发布)', + `publish_time` date DEFAULT NULL COMMENT '发布时间', + `read_count` int(11) DEFAULT 0 COMMENT '阅读数据', + `del_flg` varchar(1) DEFAULT '0' COMMENT '删除标志(0-正常/1-已删除)', + `update_time` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间', + `create_at` timestamp NULL DEFAULT current_timestamp() COMMENT '创建时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='企业文章表'; + + +# 旧版本表结构 CREATE TABLE `enterprise_news_article` ( `id` varchar(32) NOT NULL COMMENT '唯一标识符', `domain_name` varchar(64) NOT NULL COMMENT '所属域名', diff --git a/b/news/front_news_detail.dspy b/b/news/front_news_detail.dspy index e87c781..bd7cbe4 100644 --- a/b/news/front_news_detail.dspy +++ b/b/news/front_news_detail.dspy @@ -11,7 +11,9 @@ async def front_news_detail(ns={}): update_sql = """update enterprise_news_article set read_count = ifnull(read_count, 0) + 1 where id = '%s' and status = '1' and del_flg = '0';""" % ns.get('id') await sor.sqlExe(update_sql, {}) search_sql = """ - select id, title, article_type, summary, cover_img, content, publish_time, read_count + select id, title_zh, title_en, article_type_zh, article_type_en, + summary_zh, summary_en, cover_img, content_zh, content_en, + publish_time, read_count from enterprise_news_article where id = '%s' and status = '1' and del_flg = '0'; """ % ns.get('id') diff --git a/b/news/front_news_search.dspy b/b/news/front_news_search.dspy index 67d05f4..19b1035 100644 --- a/b/news/front_news_search.dspy +++ b/b/news/front_news_search.dspy @@ -13,10 +13,14 @@ async def front_news_search(ns={}): offset = (current_page - 1) * page_size conditions = ["domain_name = '%s'" % domain_name, "status = '1'", "del_flg = '0'"] - if ns.get('article_type'): - conditions.append("article_type = '%s'" % ns.get('article_type')) - if ns.get('title'): - conditions.append("title like '%%%%%s%%%%'" % ns.get('title')) + if ns.get('article_type_zh'): + conditions.append("article_type_zh = '%s'" % ns.get('article_type_zh')) + if ns.get('article_type_en'): + conditions.append("article_type_en = '%s'" % ns.get('article_type_en')) + if ns.get('title_zh'): + conditions.append("title_zh like '%%%%%s%%%%'" % ns.get('title_zh')) + if ns.get('title_en'): + conditions.append("title_en like '%%%%%s%%%%'" % ns.get('title_en')) where_clause = " and ".join(conditions) db = DBPools() @@ -25,7 +29,8 @@ async def front_news_search(ns={}): count_sql = """select count(*) as total_count from enterprise_news_article where %s;""" % where_clause total_count = (await sor.sqlExe(count_sql, {}))[0]['total_count'] search_sql = """ - select id, title, article_type, summary, cover_img, publish_time, read_count + select id, title_zh, title_en, article_type_zh, article_type_en, + summary_zh, summary_en, cover_img, publish_time, read_count from enterprise_news_article where %s order by publish_time desc, update_time desc diff --git a/b/news/news_article_add.dspy b/b/news/news_article_add.dspy index cd6a313..cf4b4d3 100644 --- a/b/news/news_article_add.dspy +++ b/b/news/news_article_add.dspy @@ -4,16 +4,11 @@ async def news_article_add(ns={}): 'status': False, 'msg': '请传递url_link' } - if not ns.get('title'): + if not ns.get('title_zh'): return { 'status': False, - 'msg': '请传递标题' + 'msg': '请传递中文标题' } - # if ns.get('article_type') not in ['企业动态', '产品动态', '行业洞察', '活动资讯']: - # return { - # 'status': False, - # 'msg': '文章类型错误' - # } domain_name = ns.get('url_link').split("//")[1].split("/")[0] if 'localhost' in domain_name: @@ -26,11 +21,15 @@ async def news_article_add(ns={}): ns_dic = { 'id': uuid(), 'domain_name': domain_name, - 'title': ns.get('title'), - 'article_type': ns.get('article_type'), - 'summary': ns.get('summary'), + 'title_zh': ns.get('title_zh'), + 'title_en': ns.get('title_en'), + 'article_type_zh': ns.get('article_type_zh'), + 'article_type_en': ns.get('article_type_en'), + 'summary_zh': ns.get('summary_zh'), + 'summary_en': ns.get('summary_en'), 'cover_img': ns.get('cover_img'), - 'content': ns.get('content'), + 'content_zh': ns.get('content_zh'), + 'content_en': ns.get('content_en'), 'status': status, 'publish_time': ns.get('publish_time'), 'read_count': 0, diff --git a/b/news/news_article_search.dspy b/b/news/news_article_search.dspy index de14ded..72ee9f4 100644 --- a/b/news/news_article_search.dspy +++ b/b/news/news_article_search.dspy @@ -14,10 +14,14 @@ async def news_article_search(ns={}): conditions = ["domain_name = '%s'" % domain_name, "del_flg = '0'"] summary_conditions = ["domain_name = '%s'" % domain_name, "del_flg = '0'"] - if ns.get('title'): - conditions.append("title like '%%%%%s%%%%'" % ns.get('title')) - if ns.get('article_type'): - conditions.append("article_type = '%s'" % ns.get('article_type')) + if ns.get('title_zh'): + conditions.append("title_zh like '%%%%%s%%%%'" % ns.get('title_zh')) + if ns.get('title_en'): + conditions.append("title_en like '%%%%%s%%%%'" % ns.get('title_en')) + if ns.get('article_type_zh'): + conditions.append("article_type_zh = '%s'" % ns.get('article_type_zh')) + if ns.get('article_type_en'): + conditions.append("article_type_en = '%s'" % ns.get('article_type_en')) if ns.get('status'): conditions.append("status = '%s'" % ns.get('status')) where_clause = " and ".join(conditions) @@ -27,26 +31,46 @@ async def news_article_search(ns={}): try: count_sql = """select count(*) as total_count from enterprise_news_article where %s;""" % where_clause total_count = (await sor.sqlExe(count_sql, {}))[0]['total_count'] - summary_sql = """ - select article_type, count(*) as article_count, ifnull(sum(read_count), 0) as read_count + summary_zh_sql = """ + select article_type_zh as article_type, count(*) as article_count, + ifnull(sum(read_count), 0) as read_count from enterprise_news_article where %s - group by article_type; + and article_type_zh is not null and article_type_zh != '' + group by article_type_zh; """ % " and ".join(summary_conditions) - summary_result = await sor.sqlExe(summary_sql, {}) - summary_mapping = {} - for summary_dic in summary_result: - summary_mapping[summary_dic.get('article_type')] = summary_dic - article_type_summary = [] + summary_en_sql = """ + select article_type_en as article_type, count(*) as article_count, + ifnull(sum(read_count), 0) as read_count + from enterprise_news_article + where %s + and article_type_en is not null and article_type_en != '' + group by article_type_en + order by article_type_en; + """ % " and ".join(summary_conditions) + summary_zh_result = await sor.sqlExe(summary_zh_sql, {}) + summary_en_result = await sor.sqlExe(summary_en_sql, {}) + summary_zh_mapping = {} + for summary_dic in summary_zh_result: + summary_zh_mapping[summary_dic.get('article_type')] = summary_dic + article_type_summary_zh = [] for article_type in ['企业动态', '产品动态', '行业洞察', '活动资讯']: - summary_dic = summary_mapping.get(article_type, {}) - article_type_summary.append({ + summary_dic = summary_zh_mapping.get(article_type, {}) + article_type_summary_zh.append({ 'article_type': article_type, 'article_count': summary_dic.get('article_count', 0), 'read_count': summary_dic.get('read_count', 0) }) + article_type_summary_en = [] + for summary_dic in summary_en_result: + article_type_summary_en.append({ + 'article_type': summary_dic.get('article_type'), + 'article_count': summary_dic.get('article_count', 0), + 'read_count': summary_dic.get('read_count', 0) + }) search_sql = """ - select id, domain_name, title, article_type, summary, cover_img, status, content, + select id, domain_name, title_zh, title_en, article_type_zh, article_type_en, + summary_zh, summary_en, cover_img, content_zh, content_en, status, publish_time, read_count, update_time, create_at from enterprise_news_article where %s @@ -61,7 +85,8 @@ async def news_article_search(ns={}): 'status': True, 'msg': 'search news article success', 'data': result, - 'article_type_summary': article_type_summary, + 'article_type_summary_zh': article_type_summary_zh, + 'article_type_summary_en': article_type_summary_en, 'pagination': { 'total': total_count, 'page_size': page_size, diff --git a/b/news/news_article_update.dspy b/b/news/news_article_update.dspy index 1977efc..a8eac0c 100644 --- a/b/news/news_article_update.dspy +++ b/b/news/news_article_update.dspy @@ -8,21 +8,14 @@ async def news_article_update(ns={}): ns_dic = { 'id': ns.get('id') } - if 'title' in ns: - ns_dic['title'] = ns.get('title') - if 'article_type' in ns: - # if ns.get('article_type') not in ['企业动态', '产品动态', '行业洞察', '活动资讯']: - # return { - # 'status': False, - # 'msg': '文章类型错误' - # } - ns_dic['article_type'] = ns.get('article_type') - if 'summary' in ns: - ns_dic['summary'] = ns.get('summary') + for field in [ + 'title_zh', 'title_en', 'article_type_zh', 'article_type_en', + 'summary_zh', 'summary_en', 'content_zh', 'content_en' + ]: + if field in ns: + ns_dic[field] = ns.get(field) 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 { diff --git a/docs/superpowers/plans/2026-07-29-bilingual-enterprise-news.md b/docs/superpowers/plans/2026-07-29-bilingual-enterprise-news.md new file mode 100644 index 0000000..90725ec --- /dev/null +++ b/docs/superpowers/plans/2026-07-29-bilingual-enterprise-news.md @@ -0,0 +1,187 @@ +# Bilingual Enterprise News Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Update enterprise news APIs to store, filter, and return Chinese and English article fields without maintaining legacy content fields. + +**Architecture:** Keep the existing DSPY endpoint structure and database table. Add and update endpoints write the eight language-specific columns; search and detail endpoints return those columns directly. Shared publication, image, date, status, and read-count behavior remains unchanged. + +**Tech Stack:** DSPY Python endpoints, async DBPools/sor database access, MySQL. + +--- + +### Task 1: Add and update bilingual content + +**Files:** +- Modify: `b/news/news_article_add.dspy` +- Modify: `b/news/news_article_update.dspy` + +- [ ] **Step 1: Update add validation** + +Replace the legacy `title` requirement with: + +```python +if not ns.get('title_zh'): + return { + 'status': False, + 'msg': '请传递中文标题' + } +``` + +- [ ] **Step 2: Update add insert fields** + +Write these fields in `ns_dic`: + +```python +'title_zh': ns.get('title_zh'), +'title_en': ns.get('title_en'), +'article_type_zh': ns.get('article_type_zh'), +'article_type_en': ns.get('article_type_en'), +'summary_zh': ns.get('summary_zh'), +'summary_en': ns.get('summary_en'), +'content_zh': ns.get('content_zh'), +'content_en': ns.get('content_en'), +``` + +Do not write `title`, `article_type`, `summary`, or `content`. + +- [ ] **Step 3: Update editable fields** + +In `news_article_update.dspy`, loop through the eight bilingual fields and copy only keys present in `ns`: + +```python +for field in [ + 'title_zh', 'title_en', 'article_type_zh', 'article_type_en', + 'summary_zh', 'summary_en', 'content_zh', 'content_en' +]: + if field in ns: + ns_dic[field] = ns.get(field) +``` + +- [ ] **Step 4: Verify static diagnostics** + +Run IDE lint diagnostics for both files. Expected: no new errors. + +### Task 2: Update backend search and detail + +**Files:** +- Modify: `b/news/news_article_search.dspy` +- Verify: `b/news/news_article_detail.dspy` + +- [ ] **Step 1: Add bilingual filters** + +Build optional conditions for exact type matching and fuzzy title matching: + +```python +if ns.get('title_zh'): + conditions.append("title_zh like '%%%%%s%%%%'" % ns.get('title_zh')) +if ns.get('title_en'): + conditions.append("title_en like '%%%%%s%%%%'" % ns.get('title_en')) +if ns.get('article_type_zh'): + conditions.append("article_type_zh = '%s'" % ns.get('article_type_zh')) +if ns.get('article_type_en'): + conditions.append("article_type_en = '%s'" % ns.get('article_type_en')) +``` + +- [ ] **Step 2: Return bilingual list fields** + +Select: + +```sql +id, domain_name, +title_zh, title_en, +article_type_zh, article_type_en, +summary_zh, summary_en, +content_zh, content_en, +cover_img, status, publish_time, read_count, update_time, create_at +``` + +- [ ] **Step 3: Return bilingual type summaries** + +Aggregate Chinese and English article types separately and return: + +```python +'article_type_summary_zh': article_type_summary_zh, +'article_type_summary_en': article_type_summary_en, +``` + +Each item keeps `article_type`, `article_count`, and `read_count`. + +- [ ] **Step 4: Verify backend detail** + +`news_article_detail.dspy` already uses `select *`; confirm it returns the new columns without changing publication behavior. + +- [ ] **Step 5: Verify static diagnostics** + +Run IDE lint diagnostics for backend search and detail. Expected: no new errors. + +### Task 3: Update frontend search and detail + +**Files:** +- Modify: `b/news/front_news_search.dspy` +- Modify: `b/news/front_news_detail.dspy` + +- [ ] **Step 1: Add bilingual frontend filters** + +Support `title_zh`, `title_en`, `article_type_zh`, and `article_type_en` using the same matching rules as backend search. + +- [ ] **Step 2: Return bilingual frontend list fields** + +Select: + +```sql +id, title_zh, title_en, article_type_zh, article_type_en, +summary_zh, summary_en, cover_img, publish_time, read_count +``` + +- [ ] **Step 3: Return bilingual frontend detail fields** + +After incrementing `read_count`, select: + +```sql +id, title_zh, title_en, article_type_zh, article_type_en, +summary_zh, summary_en, cover_img, content_zh, content_en, +publish_time, read_count +``` + +- [ ] **Step 4: Verify publication and read-count behavior** + +Confirm both frontend queries retain `status = '1' AND del_flg = '0'`, and detail still increments `read_count` once. + +- [ ] **Step 5: Verify static diagnostics** + +Run IDE lint diagnostics for both frontend files. Expected: no new errors. + +### Task 4: Final verification + +**Files:** +- Verify: `b/news/news_article_add.dspy` +- Verify: `b/news/news_article_update.dspy` +- Verify: `b/news/news_article_search.dspy` +- Verify: `b/news/news_article_detail.dspy` +- Verify: `b/news/front_news_search.dspy` +- Verify: `b/news/front_news_detail.dspy` + +- [ ] **Step 1: Check legacy-field removal** + +Search the six endpoints for writes or selected output fields named exactly `title`, `article_type`, `summary`, or `content`. Expected: none except comments or compatibility-neutral code. + +- [ ] **Step 2: Check diff scope** + +Run: + +```powershell +git diff -- b/news +``` + +Expected: only the requested bilingual endpoint changes plus the user's existing SQL schema update. + +- [ ] **Step 3: Check whitespace** + +Run: + +```powershell +git diff --check -- b/news +``` + +Expected: no output and exit code 0. From 15079ad7301e1f0f775f43c2bff128f0e49f1783 Mon Sep 17 00:00:00 2001 From: ping <1017253325@qq.com> Date: Wed, 29 Jul 2026 16:55:37 +0800 Subject: [PATCH 3/9] docs: extend bilingual news media fields Co-authored-by: Cursor --- .../2026-07-29-bilingual-enterprise-news-design.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/specs/2026-07-29-bilingual-enterprise-news-design.md b/docs/superpowers/specs/2026-07-29-bilingual-enterprise-news-design.md index 851e2a5..edce5b3 100644 --- a/docs/superpowers/specs/2026-07-29-bilingual-enterprise-news-design.md +++ b/docs/superpowers/specs/2026-07-29-bilingual-enterprise-news-design.md @@ -10,9 +10,12 @@ - 中文:`title_zh`、`article_type_zh`、`summary_zh`、`content_zh` - 英文:`title_en`、`article_type_en`、`summary_en`、`content_en` -- 共用:`cover_img`、`status`、`publish_time`、`read_count` +- 中文扩展:`cover_img_zh`、`publish_time_zh` +- 英文扩展:`cover_img_en`、`publish_time_en` +- 共用:`status`、`read_count` 新增文章仅要求 `title_zh` 必填,其余中英文字段允许为空。 +文章发布时,如果未传中英文发布时间,两个字段都默认当天日期。 ## 接口调整 @@ -22,16 +25,20 @@ - `news_article_detail.dspy`:返回全部中英文字段。 - `front_news_search.dspy`:支持中英文标题和类型筛选,返回全部中英文字段。 - `front_news_detail.dspy`:返回全部中英文字段,并保持阅读量加一。 +- `news_article_publish.dspy`:支持分别设置中英文发布时间,未传时均默认当天。 -发布、下架、删除接口不涉及语言字段,不调整。 +下架、删除接口不涉及语言字段,不调整。 ## 兼容边界 旧字段已由数据库移除非空约束,本次接口不再维护旧字段。调用方需要改为传入和读取中英文新字段。 +旧字段 `cover_img`、`publish_time` 同样不再读写。列表按两个发布时间中的较晚日期倒序排列,再按更新时间倒序排列。 ## 验证 - 新增时缺少 `title_zh` 返回校验错误。 - 新增、更新后详情能完整返回中英文字段。 - 后台和前端列表能按中英文标题、类型筛选。 +- 新增、编辑、列表和详情能分别读写中英文封面及发布时间。 +- 发布时未传中英文发布时间,两个字段均写入当天日期。 - 前端详情仍仅允许读取已发布文章,并正确增加阅读量。 From a66a9ee56cf443c90a228a627d4bcaf561b3347b Mon Sep 17 00:00:00 2001 From: ping <1017253325@qq.com> Date: Wed, 29 Jul 2026 17:36:49 +0800 Subject: [PATCH 4/9] update --- b/news/enterprise_news_article.sql | 6 ++- b/news/front_news_detail.dspy | 4 +- b/news/front_news_search.dspy | 8 +++- b/news/news_article_add.dspy | 15 ++++-- b/news/news_article_publish.dspy | 14 ++++-- b/news/news_article_search.dspy | 9 ++-- b/news/news_article_update.dspy | 17 +++---- .../2026-07-29-bilingual-enterprise-news.md | 46 +++++++++++++++++++ 8 files changed, 94 insertions(+), 25 deletions(-) diff --git a/b/news/enterprise_news_article.sql b/b/news/enterprise_news_article.sql index e6fea1f..4b264db 100644 --- a/b/news/enterprise_news_article.sql +++ b/b/news/enterprise_news_article.sql @@ -1,3 +1,4 @@ +# 新版本表结构 CREATE TABLE `enterprise_news_article` ( `id` varchar(32) NOT NULL COMMENT '唯一标识符', `domain_name` varchar(64) NOT NULL COMMENT '所属域名', @@ -9,6 +10,10 @@ CREATE TABLE `enterprise_news_article` ( `article_type_en` varchar(20) DEFAULT NULL COMMENT '英文文章类型', `content_zh` text DEFAULT NULL COMMENT '中文正文', `content_en` text DEFAULT NULL COMMENT '英文正文', + `cover_img_zh` varchar(255) DEFAULT NULL COMMENT '中文封面图片', + `cover_img_en` varchar(255) DEFAULT NULL COMMENT '英文封面图片', + `publish_time_zh` date DEFAULT NULL COMMENT '中文发布时间', + `publish_time_en` date DEFAULT NULL COMMENT '英文发布时间', `title` varchar(100) DEFAULT NULL COMMENT '文章标题', `article_type` varchar(20) DEFAULT NULL COMMENT '文章类型(企业动态/产品动态/行业洞察/活动资讯)', `summary` varchar(255) DEFAULT NULL COMMENT '文章摘要', @@ -23,7 +28,6 @@ CREATE TABLE `enterprise_news_article` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='企业文章表'; - # 旧版本表结构 CREATE TABLE `enterprise_news_article` ( `id` varchar(32) NOT NULL COMMENT '唯一标识符', diff --git a/b/news/front_news_detail.dspy b/b/news/front_news_detail.dspy index bd7cbe4..fb19531 100644 --- a/b/news/front_news_detail.dspy +++ b/b/news/front_news_detail.dspy @@ -12,8 +12,8 @@ async def front_news_detail(ns={}): await sor.sqlExe(update_sql, {}) search_sql = """ select id, title_zh, title_en, article_type_zh, article_type_en, - summary_zh, summary_en, cover_img, content_zh, content_en, - publish_time, read_count + summary_zh, summary_en, cover_img_zh, cover_img_en, + content_zh, content_en, publish_time_zh, publish_time_en, read_count from enterprise_news_article where id = '%s' and status = '1' and del_flg = '0'; """ % ns.get('id') diff --git a/b/news/front_news_search.dspy b/b/news/front_news_search.dspy index 19b1035..c24eb05 100644 --- a/b/news/front_news_search.dspy +++ b/b/news/front_news_search.dspy @@ -30,10 +30,14 @@ async def front_news_search(ns={}): total_count = (await sor.sqlExe(count_sql, {}))[0]['total_count'] search_sql = """ select id, title_zh, title_en, article_type_zh, article_type_en, - summary_zh, summary_en, cover_img, publish_time, read_count + summary_zh, summary_en, cover_img_zh, cover_img_en, + publish_time_zh, publish_time_en, read_count from enterprise_news_article where %s - order by publish_time desc, update_time desc + order by greatest( + ifnull(publish_time_zh, '1000-01-01'), + ifnull(publish_time_en, '1000-01-01') + ) desc, update_time desc limit %s offset %s; """ % (where_clause, page_size, offset) result = await sor.sqlExe(search_sql, {}) diff --git a/b/news/news_article_add.dspy b/b/news/news_article_add.dspy index cf4b4d3..680041d 100644 --- a/b/news/news_article_add.dspy +++ b/b/news/news_article_add.dspy @@ -27,11 +27,13 @@ async def news_article_add(ns={}): 'article_type_en': ns.get('article_type_en'), 'summary_zh': ns.get('summary_zh'), 'summary_en': ns.get('summary_en'), - 'cover_img': ns.get('cover_img'), + 'cover_img_zh': ns.get('cover_img_zh'), + 'cover_img_en': ns.get('cover_img_en'), 'content_zh': ns.get('content_zh'), 'content_en': ns.get('content_en'), 'status': status, - 'publish_time': ns.get('publish_time'), + 'publish_time_zh': ns.get('publish_time_zh'), + 'publish_time_en': ns.get('publish_time_en'), 'read_count': 0, 'del_flg': '0' } @@ -40,8 +42,13 @@ async def news_article_add(ns={}): async with db.sqlorContext('kboss') as sor: try: await sor.C('enterprise_news_article', ns_dic) - if status == '1' and not ns.get('publish_time'): - publish_sql = """update enterprise_news_article set publish_time = current_timestamp() where id = '%s';""" % ns_dic.get('id') + if status == '1': + publish_sql = """ + update enterprise_news_article + set publish_time_zh = ifnull(publish_time_zh, current_date()), + publish_time_en = ifnull(publish_time_en, current_date()) + where id = '%s'; + """ % ns_dic.get('id') await sor.sqlExe(publish_sql, {}) return { 'status': True, diff --git a/b/news/news_article_publish.dspy b/b/news/news_article_publish.dspy index 219ab3a..853edf1 100644 --- a/b/news/news_article_publish.dspy +++ b/b/news/news_article_publish.dspy @@ -8,11 +8,15 @@ async def news_article_publish(ns={}): db = DBPools() async with db.sqlorContext('kboss') as sor: try: - publish_time = ns.get('publish_time') - if publish_time: - publish_sql = """update enterprise_news_article set status = '1', publish_time = '%s' where id = '%s' and del_flg = '0';""" % (publish_time, ns.get('id')) - else: - publish_sql = """update enterprise_news_article set status = '1' where id = '%s' and del_flg = '0';""" % ns.get('id') + publish_time_zh = "'%s'" % ns.get('publish_time_zh') if ns.get('publish_time_zh') else "ifnull(publish_time_zh, current_date())" + publish_time_en = "'%s'" % ns.get('publish_time_en') if ns.get('publish_time_en') else "ifnull(publish_time_en, current_date())" + publish_sql = """ + update enterprise_news_article + set status = '1', + publish_time_zh = %s, + publish_time_en = %s + where id = '%s' and del_flg = '0'; + """ % (publish_time_zh, publish_time_en, ns.get('id')) await sor.sqlExe(publish_sql, {}) return { 'status': True, diff --git a/b/news/news_article_search.dspy b/b/news/news_article_search.dspy index 72ee9f4..bfbc28a 100644 --- a/b/news/news_article_search.dspy +++ b/b/news/news_article_search.dspy @@ -70,11 +70,14 @@ async def news_article_search(ns={}): }) search_sql = """ select id, domain_name, title_zh, title_en, article_type_zh, article_type_en, - summary_zh, summary_en, cover_img, content_zh, content_en, status, - publish_time, read_count, update_time, create_at + summary_zh, summary_en, cover_img_zh, cover_img_en, content_zh, content_en, + status, publish_time_zh, publish_time_en, read_count, update_time, create_at from enterprise_news_article where %s - order by publish_time desc, update_time desc + order by greatest( + ifnull(publish_time_zh, '1000-01-01'), + ifnull(publish_time_en, '1000-01-01') + ) desc, update_time desc limit %s offset %s; """ % (where_clause, page_size, offset) result = await sor.sqlExe(search_sql, {}) diff --git a/b/news/news_article_update.dspy b/b/news/news_article_update.dspy index a8eac0c..ae3d493 100644 --- a/b/news/news_article_update.dspy +++ b/b/news/news_article_update.dspy @@ -10,12 +10,11 @@ async def news_article_update(ns={}): } for field in [ 'title_zh', 'title_en', 'article_type_zh', 'article_type_en', - 'summary_zh', 'summary_en', 'content_zh', 'content_en' + 'summary_zh', 'summary_en', 'content_zh', 'content_en', + 'cover_img_zh', 'cover_img_en', 'publish_time_zh', 'publish_time_en' ]: if field in ns: ns_dic[field] = ns.get(field) - if 'cover_img' in ns: - ns_dic['cover_img'] = ns.get('cover_img') if 'status' in ns: if ns.get('status') not in ['0', '1']: return { @@ -23,15 +22,17 @@ async def news_article_update(ns={}): 'msg': '状态错误' } ns_dic['status'] = ns.get('status') - if 'publish_time' in ns: - ns_dic['publish_time'] = ns.get('publish_time') - db = DBPools() async with db.sqlorContext('kboss') as sor: try: await sor.U('enterprise_news_article', ns_dic) - if ns.get('status') == '1' and not ns.get('publish_time'): - publish_sql = """update enterprise_news_article set publish_time = ifnull(publish_time, current_timestamp()) where id = '%s';""" % ns.get('id') + if ns.get('status') == '1': + publish_sql = """ + update enterprise_news_article + set publish_time_zh = ifnull(publish_time_zh, current_date()), + publish_time_en = ifnull(publish_time_en, current_date()) + where id = '%s'; + """ % ns.get('id') await sor.sqlExe(publish_sql, {}) return { 'status': True, diff --git a/docs/superpowers/plans/2026-07-29-bilingual-enterprise-news.md b/docs/superpowers/plans/2026-07-29-bilingual-enterprise-news.md index 90725ec..96fe9a6 100644 --- a/docs/superpowers/plans/2026-07-29-bilingual-enterprise-news.md +++ b/docs/superpowers/plans/2026-07-29-bilingual-enterprise-news.md @@ -185,3 +185,49 @@ git diff --check -- b/news ``` Expected: no output and exit code 0. + +### Task 5: Add bilingual cover images and publish times + +**Files:** +- Modify: `b/news/news_article_add.dspy` +- Modify: `b/news/news_article_update.dspy` +- Modify: `b/news/news_article_publish.dspy` +- Modify: `b/news/news_article_search.dspy` +- Modify: `b/news/front_news_search.dspy` +- Modify: `b/news/front_news_detail.dspy` + +- [ ] **Step 1: Replace shared write fields** + +Use `cover_img_zh`, `cover_img_en`, `publish_time_zh`, and +`publish_time_en` in add and update. Do not write `cover_img` or +`publish_time`. + +- [ ] **Step 2: Apply publication defaults** + +When status becomes `1`, set each missing publish-time column with: + +```sql +publish_time_zh = ifnull(publish_time_zh, current_date()), +publish_time_en = ifnull(publish_time_en, current_date()) +``` + +- [ ] **Step 3: Update publish endpoint** + +Accept both publish-time parameters and use `current_date()` for either +missing value while setting `status = '1'`. + +- [ ] **Step 4: Update list and detail output** + +Return both cover-image and publish-time columns. Sort lists by: + +```sql +greatest( + ifnull(publish_time_zh, '1000-01-01'), + ifnull(publish_time_en, '1000-01-01') +) desc, update_time desc +``` + +- [ ] **Step 5: Verify** + +Run static field-contract checks, IDE diagnostics, and +`git diff --check -- b/news`. Expected: all pass. From 65d5dc0920c8bb7cfcb8252219e1f3d11bc9b5c6 Mon Sep 17 00:00:00 2001 From: ping <1017253325@qq.com> Date: Wed, 29 Jul 2026 17:44:32 +0800 Subject: [PATCH 5/9] update --- b/news/news_article_add.dspy | 4 ++-- b/news/news_article_update.dspy | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/b/news/news_article_add.dspy b/b/news/news_article_add.dspy index 680041d..6fd8518 100644 --- a/b/news/news_article_add.dspy +++ b/b/news/news_article_add.dspy @@ -32,8 +32,8 @@ async def news_article_add(ns={}): 'content_zh': ns.get('content_zh'), 'content_en': ns.get('content_en'), 'status': status, - 'publish_time_zh': ns.get('publish_time_zh'), - 'publish_time_en': ns.get('publish_time_en'), + 'publish_time_zh': ns.get('publish_time_zh') or None, + 'publish_time_en': ns.get('publish_time_en') or None, 'read_count': 0, 'del_flg': '0' } diff --git a/b/news/news_article_update.dspy b/b/news/news_article_update.dspy index ae3d493..4c9bef6 100644 --- a/b/news/news_article_update.dspy +++ b/b/news/news_article_update.dspy @@ -14,7 +14,10 @@ async def news_article_update(ns={}): 'cover_img_zh', 'cover_img_en', 'publish_time_zh', 'publish_time_en' ]: if field in ns: - ns_dic[field] = ns.get(field) + if field in ['publish_time_zh', 'publish_time_en']: + ns_dic[field] = ns.get(field) or None + else: + ns_dic[field] = ns.get(field) if 'status' in ns: if ns.get('status') not in ['0', '1']: return { From 3784175a34d12d8136798a845252a7669c1811b1 Mon Sep 17 00:00:00 2001 From: hnsqzyc <1017253325@qq.com> Date: Thu, 30 Jul 2026 17:24:33 +0800 Subject: [PATCH 6/9] update --- kgadget/src/baiDuSmsClient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kgadget/src/baiDuSmsClient.py b/kgadget/src/baiDuSmsClient.py index 3ee040b..72bdb5e 100644 --- a/kgadget/src/baiDuSmsClient.py +++ b/kgadget/src/baiDuSmsClient.py @@ -33,8 +33,8 @@ class BaiduSMS: # self.signature_id = 'sms-sign-BqOhYB33019' # 开元云 # self.signature_id = 'sms-sign-LOShPq75464' # 开元云北京 # self.signature_id = 'sms-sign-xQYUwp42637' # 开元云北京 - self.signature_id = 'sms-sign-SyPAar57327' # 开元云北京科技 - # self.signature_id = 'sms-sign-JEimHH86684' # 开元数智北京科技 + # self.signature_id = 'sms-sign-SyPAar57327' # 开元云北京科技 + self.signature_id = 'sms-sign-JEimHH86684' # 开元数智北京科技 # 短信模板类型映射(键为业务类型,值为对应模板ID) self.sms_types = { "注册登录验证": "sms-tpl-123", # 示例模板ID From 94b335c3ed2a540101172ca20f003f6f9769deca Mon Sep 17 00:00:00 2001 From: hnsqzyc <1017253325@qq.com> Date: Thu, 30 Jul 2026 17:44:15 +0800 Subject: [PATCH 7/9] update --- kgadget/src/baiDuSmsClient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kgadget/src/baiDuSmsClient.py b/kgadget/src/baiDuSmsClient.py index 72bdb5e..721dc3c 100644 --- a/kgadget/src/baiDuSmsClient.py +++ b/kgadget/src/baiDuSmsClient.py @@ -32,9 +32,9 @@ class BaiduSMS: # self.signature_id = 'sms-sign-qQHYeC17077' # 开元云科技 # self.signature_id = 'sms-sign-BqOhYB33019' # 开元云 # self.signature_id = 'sms-sign-LOShPq75464' # 开元云北京 - # self.signature_id = 'sms-sign-xQYUwp42637' # 开元云北京 + self.signature_id = 'sms-sign-xQYUwp42637' # 开元云北京 # self.signature_id = 'sms-sign-SyPAar57327' # 开元云北京科技 - self.signature_id = 'sms-sign-JEimHH86684' # 开元数智北京科技 + # self.signature_id = 'sms-sign-JEimHH86684' # 开元数智北京科技 # 短信模板类型映射(键为业务类型,值为对应模板ID) self.sms_types = { "注册登录验证": "sms-tpl-123", # 示例模板ID From c8f568d8eb807e72168cbba202a61e218d27b946 Mon Sep 17 00:00:00 2001 From: hnsqzyc <1017253325@qq.com> Date: Thu, 30 Jul 2026 20:54:08 +0800 Subject: [PATCH 8/9] update --- kgadget/src/baiDuSmsClient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kgadget/src/baiDuSmsClient.py b/kgadget/src/baiDuSmsClient.py index 721dc3c..3ee040b 100644 --- a/kgadget/src/baiDuSmsClient.py +++ b/kgadget/src/baiDuSmsClient.py @@ -32,8 +32,8 @@ class BaiduSMS: # self.signature_id = 'sms-sign-qQHYeC17077' # 开元云科技 # self.signature_id = 'sms-sign-BqOhYB33019' # 开元云 # self.signature_id = 'sms-sign-LOShPq75464' # 开元云北京 - self.signature_id = 'sms-sign-xQYUwp42637' # 开元云北京 - # self.signature_id = 'sms-sign-SyPAar57327' # 开元云北京科技 + # self.signature_id = 'sms-sign-xQYUwp42637' # 开元云北京 + self.signature_id = 'sms-sign-SyPAar57327' # 开元云北京科技 # self.signature_id = 'sms-sign-JEimHH86684' # 开元数智北京科技 # 短信模板类型映射(键为业务类型,值为对应模板ID) self.sms_types = { From 56f2300ba356b0c323d716b11d774ecaae95a7d8 Mon Sep 17 00:00:00 2001 From: hrx <18603305412@163.com> Date: Fri, 31 Jul 2026 15:07:23 +0800 Subject: [PATCH 9/9] =?UTF-8?q?7.31=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- f/web-kboss/src/i18n/lang/en-US.js | 2 +- .../homePage/components/topBox/index.vue | 13 +- .../src/views/homePage/mainPage/index.vue | 107 +++- .../homePage/news/ArticleEditorDialog.vue | 506 +++++++++++++++++- .../src/views/homePage/news/NewsDetail.vue | 26 +- .../src/views/homePage/news/newsLog.vue | 46 +- .../src/views/homePage/news/newsView.vue | 38 +- .../login/components/ForgotPasswordDialog.vue | 46 +- 8 files changed, 695 insertions(+), 89 deletions(-) diff --git a/f/web-kboss/src/i18n/lang/en-US.js b/f/web-kboss/src/i18n/lang/en-US.js index 2e01858..8db69f6 100644 --- a/f/web-kboss/src/i18n/lang/en-US.js +++ b/f/web-kboss/src/i18n/lang/en-US.js @@ -18,7 +18,7 @@ export default { switchEnSuccess: 'Switched to English' }, home: { - heroTitle: 'One Platform · Intelligent Leap · Across All Industries.', + heroTitle: 'Make AI Everywhere, Make AI Easy', heroSubtitle: '', heroSlogan: 'Make AI Everywhere, Make AI Easy', solutionsBtn: 'Solutions', diff --git a/f/web-kboss/src/views/homePage/components/topBox/index.vue b/f/web-kboss/src/views/homePage/components/topBox/index.vue index 4971328..147670b 100644 --- a/f/web-kboss/src/views/homePage/components/topBox/index.vue +++ b/f/web-kboss/src/views/homePage/components/topBox/index.vue @@ -57,6 +57,14 @@ +

{{ $t('home.heroSubtitle') }}

-

{{ $t('home.heroSlogan') }}

+
{{ $t('home.contactSalesBtn') }}
@@ -37,6 +37,14 @@ @mouseleave="startSolutionCarousel" @click="setSolutionIndex(item.index)" > +

{{ item.card.title }}

@@ -71,7 +79,7 @@
- +
@@ -101,7 +109,7 @@
- +
@@ -129,7 +137,7 @@
' }, { @@ -303,6 +313,8 @@ export default { ], bg: 'linear-gradient(145deg, rgba(200, 214, 242, 0.72) 0%, rgba(158, 180, 230, 0.64) 44%, rgba(116, 146, 216, 0.58) 100%)', shadow: '0 18px 54px rgba(60, 80, 160, 0.18), 0 4px 16px rgba(60, 80, 160, 0.08)', + scenarioLabel: '合同智能审查→', + scenarioPath: '/homePage/agentStore/contractCase', icon: '' }, { @@ -378,7 +390,29 @@ export default { }) }, localizedNewsList() { - return this.newsList + const isEnglish = this.$i18n && this.$i18n.locale === 'en-US' + return this.newsList.map((item) => { + const tag = isEnglish + ? (item.tagEn || item.tagZh || this.$t('home.news.tag')) + : (item.tagZh || item.tagEn || this.$t('home.news.tag')) + const title = isEnglish + ? (item.titleEn || item.titleZh || '') + : (item.titleZh || item.titleEn || '') + const desc = isEnglish + ? (item.descEn || item.descZh || '') + : (item.descZh || item.descEn || '') + const date = isEnglish + ? (item.dateEn || item.dateZh || '') + : (item.dateZh || item.dateEn || '') + + return { + ...item, + tag, + title, + desc, + date + } + }) }, visibleSolutionCards() { const slots = [-3, -2, -1, 0, 1, 2, 3] @@ -430,6 +464,9 @@ export default { block: 'start' }) }, + goSolutionScenario(path) { + this.navigateTo(path) + }, contactSales() { this.$store.commit('setShowTalk', true) }, @@ -455,11 +492,15 @@ export default { normalizeHomeNewsItem(row, index) { return { id: row.id, - tag: row.article_type || this.$t('home.news.tag'), tagClass: this.getNewsTagClass(index), - date: row.publish_time || row.publishTime || '', - title: row.title || '', - desc: row.summary || row.desc || row.description || '' + tagZh: row.article_type_zh || row.article_type || '', + tagEn: row.article_type_en || '', + dateZh: row.publish_time_zh || row.publish_time || row.publishTime || '', + dateEn: row.publish_time_en || '', + titleZh: row.title_zh || row.title || '', + titleEn: row.title_en || '', + descZh: row.summary_zh || row.summary || row.desc || row.description || '', + descEn: row.summary_en || '' } }, getResponseList(res) { @@ -481,8 +522,7 @@ export default { this.newsList = [] } }, - - + nextSolution() { this.switchSolution(1) }, @@ -690,7 +730,6 @@ body.dark-theme .bg-orb { align-items: center; justify-content: center; - &:hover { background: #fff; transform: translateY(-2px); @@ -824,6 +863,39 @@ body.dark-theme .bg-orb { transition: all 0.28s ease; } +.solution-card__scenario-link { + position: absolute; + top: 16px; + right: 16px; + z-index: 3; + padding: 5px 9px; + color: #1d4ed8; + font-size: 12px; + line-height: 1.2; + font-weight: 700; + white-space: nowrap; + background: rgba(255, 255, 255, 0.58); + border: 1px solid rgba(255, 255, 255, 0.72); + border-radius: 999px; + opacity: 0; + cursor: pointer; + pointer-events: none; + transform: translateY(-4px); + transition: background 0.2s ease, color 0.2s ease, opacity 0.2s ease, transform 0.2s ease; +} + +.solution-card:hover .solution-card__scenario-link { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} + +.solution-card__scenario-link:hover { + color: #1e40af; + background: rgba(255, 255, 255, 0.9); + transform: translateY(-1px); +} + .solution-card__icon { position: relative; z-index: 1; @@ -865,15 +937,12 @@ body.dark-theme .bg-orb { transition: all 0.25s ease; overflow-x: hidden; overflow-y: auto; + -ms-overflow-style: none; + scrollbar-width: none; box-sizing: border-box; &::-webkit-scrollbar { - width: 4px; - } - - &::-webkit-scrollbar-thumb { - background: rgba(37, 99, 235, 0.18); - border-radius: 999px; + display: none; } strong { @@ -1546,8 +1615,6 @@ body.dark-theme .bg-orb { } } - - @keyframes heroFadeIn { from { opacity: 0; diff --git a/f/web-kboss/src/views/homePage/news/ArticleEditorDialog.vue b/f/web-kboss/src/views/homePage/news/ArticleEditorDialog.vue index 792d8a1..3491660 100644 --- a/f/web-kboss/src/views/homePage/news/ArticleEditorDialog.vue +++ b/f/web-kboss/src/views/homePage/news/ArticleEditorDialog.vue @@ -18,7 +18,7 @@
-
+
@@ -71,11 +71,27 @@
+
+
+ EN +
+ 多语言输入 + 添加英文版文章内容 +
+
+ +
-
-
+ +
+
+
+ ENGLISH VERSION +

英文版文章

+

英文内容会与中文内容一并保存或发布。

+
+ English +
+ +
+
+ + +
+
+ + +
+
+ + + + + + + +
+
+ + +
+
+ +
+ + + {{ english.coverName }} + +
+
+
+ +
+
+ English Content + Rich text editor +
+
+ + +
+
+
{ + customUpload: async(file, insertFn) => { try { // 正文图片上传到文件接口,正文中保存可回显的图片地址。 const url = await this.uploadNewsImage(file) @@ -178,13 +281,33 @@ export default { } } }, + englishEditorConfig: { + placeholder: 'Start writing the English version of your article...', + MENU_CONF: { + uploadImage: { + customUpload: async(file, insertFn) => { + try { + const url = await this.uploadNewsImage(file) + insertFn(normalizeImageUrl(url), file.name, normalizeImageUrl(url)) + } catch (error) { + this.$message.error('英文正文图片上传失败') + } + } + } + } + }, // 上传封面的本地预览地址,只用于页面显示,不提交给接口。 coverPreview: '', imageUploading: false, + englishImageUploading: false, editorFullscreen: false, editorFullscreenObserver: null, // 右侧预览区域宽度。 previewWidth: 400, + // 控制英文版区域展开状态。 + showEnglishForm: false, + // 英文版内容,与中文字段一起提交到文章保存接口。 + english: this.createEmptyEnglishForm(), // 标记预览区域是否正在拖拽调整宽度。 resizing: false, // 记录拖拽开始时的鼠标位置。 @@ -230,6 +353,12 @@ export default { // 任意表单字段变化后标记为未保存。 this.dirty = true } + }, + english: { + deep: true, + handler() { + this.dirty = true + } } }, beforeDestroy() { @@ -238,12 +367,16 @@ export default { this.editor.destroy() this.editor = null } + if (this.englishEditor) { + this.englishEditor.destroy() + this.englishEditor = null + } // 清理拖拽监听事件。 this.removeResizeListeners() this.stopObserveEditorFullscreen() this.editorFullscreen = false }, - methods: { + methods: { createEmptyForm() { // 创建新增文章时使用的默认表单结构。 return { @@ -259,17 +392,58 @@ export default { coverName: '' } }, + createEmptyEnglishForm() { + return { + title: '', + summary: '', + category: '', + publishTime: '', + coverUrl: '', + coverName: '', + content: '' + } + }, handleOpen() { // 弹窗打开时合并默认值和待编辑文章数据。 + const article = this.article || {} + const english = article.english || {} this.form = { ...this.createEmptyForm(), - ...(this.article || {}) + ...article, + title: article.title || article.title_zh || '', + summary: article.summary || article.summary_zh || '', + type: article.type || article.article_type_zh || '企业动态', + publishTime: article.publishTime || article.publish_time_zh || article.publish_time || '', + content: article.content || article.content_zh || '', + coverUrl: article.coverUrl || article.cover_img_zh || article.cover_img || '', + coverName: article.coverName || article.cover_name_zh || article.cover_name || '' } // 没有发布时间时默认使用当天日期。 if (!this.form.publishTime) this.form.publishTime = this.today // 初始化封面预览。 this.coverPreview = this.form.coverUrl || '' this.editorFullscreen = false + this.imageUploading = false + this.englishImageUploading = false + this.english = { + ...this.createEmptyEnglishForm(), + ...english, + title: english.title || article.title_en || '', + summary: english.summary || article.summary_en || '', + category: english.category || article.article_type_en || '', + publishTime: english.publishTime || article.publish_time_en || '', + content: english.content || article.content_en || '', + coverUrl: english.coverUrl || article.cover_img_en || '', + coverName: english.coverName || article.cover_name_en || '' + } + this.showEnglishForm = Boolean( + this.english.title || + this.english.summary || + this.english.category || + this.english.publishTime || + this.english.coverUrl || + this.english.content + ) this.$nextTick(() => { // 表单初始化完成后重置未保存状态。 this.dirty = false @@ -280,6 +454,12 @@ export default { // 保存编辑器实例,后续销毁时使用。 this.editor = Object.seal(editor) }, + handleEnglishEditorCreated(editor) { + this.englishEditor = Object.seal(editor) + }, + handleEnglishEditorDestroyed() { + this.englishEditor = null + }, handleDialogClose() { this.stopObserveEditorFullscreen() this.editorFullscreen = false @@ -398,6 +578,23 @@ export default { event.target.value = '' } }, + async handleEnglishCoverUpload(event) { + const file = event.target.files && event.target.files[0] + if (!file) return + + this.english.coverName = file.name + this.englishImageUploading = true + try { + this.english.coverUrl = await this.uploadNewsImage(file) + this.$message.success('英文封面图片上传成功') + } catch (error) { + this.english.coverName = '' + this.$message.error('英文封面图片上传失败') + } finally { + this.englishImageUploading = false + event.target.value = '' + } + }, handleCoverUrlInput() { // 用户手动输入图片路径时,清空本地上传预览状态。 this.coverPreview = '' @@ -469,13 +666,16 @@ export default { this.$message.warning('请输入文章正文') return } - if (this.imageUploading) { + if (this.imageUploading || this.englishImageUploading) { this.$message.warning('图片上传中,请稍后保存') return } // 第六步:组装保存参数,status 由按钮决定是草稿还是发布。 const payload = { ...this.form, + english: { + ...this.english + }, publish_time: this.form.publishTime, status } @@ -572,6 +772,21 @@ export default { flex-direction: column; } +.article-editor__left.has-language-form { + overflow-y: auto; + scrollbar-color: #cbd5e1 transparent; + scrollbar-width: thin; +} + +.article-editor__left.has-language-form .article-editor__form { + flex-shrink: 0; +} + +.article-editor__left.has-language-form .wang-editor-wrap { + flex: 0 0 420px; + min-height: 420px; +} + .resize-handle { position: relative; flex: 0 0 10px; @@ -614,6 +829,70 @@ export default { border-bottom: 1px solid #edf0f5; } +.language-trigger { + min-height: 48px; + margin: 2px 0 12px 88px; + padding: 9px 12px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + background: linear-gradient(90deg, #f8fbff 0%, #f7f5ff 100%); + border: 1px dashed #bfdbfe; + border-radius: 8px; +} + +.language-trigger__title { + display: flex; + align-items: center; + gap: 9px; +} + +.language-trigger__badge { + width: 26px; + height: 26px; + display: inline-flex; + align-items: center; + justify-content: center; + color: #4f46e5; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + background: #ede9fe; + border-radius: 7px; +} + +.language-trigger__title strong, +.language-trigger__title small { + display: block; +} + +.language-trigger__title strong { + color: #334155; + font-size: 13px; +} + +.language-trigger__title small { + margin-top: 2px; + color: #94a3b8; + font-size: 11px; +} + +.language-trigger__button { + padding: 5px 8px; + color: #4f46e5; + font-size: 12px; + font-weight: 600; + white-space: nowrap; + background: transparent; + border: 0; + cursor: pointer; +} + +.language-trigger__button i { + margin-left: 4px; +} + .article-editor__inline { display: flex; gap: 16px; @@ -671,6 +950,219 @@ export default { overflow-y: auto; } +.article-editor__language-section { + flex-shrink: 0; + margin-top: 22px; + padding: 24px 18px 40px 0; + border-top: 1px solid #dbeafe; +} + +.language-section-head { + margin-bottom: 18px; + padding: 16px 18px; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + background: linear-gradient(135deg, #f5f9ff 0%, #f7f5ff 100%); + border: 1px solid #dbeafe; + border-radius: 12px; +} + +.language-section-head__eyebrow { + display: block; + margin-bottom: 5px; + color: #6366f1; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.12em; +} + +.language-section-head h3 { + margin: 0; + color: #1e293b; + font-size: 16px; + line-height: 1.4; +} + +.language-section-head p { + margin: 5px 0 0; + color: #94a3b8; + font-size: 12px; +} + +.language-section-head__tag { + padding: 4px 9px; + color: #4f46e5; + font-size: 12px; + font-weight: 600; + background: #ede9fe; + border-radius: 999px; +} + +.language-form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.language-field--wide { + grid-column: 1 / -1; +} + +.language-field label { + display: block; + margin-bottom: 7px; + color: #475569; + font-size: 12px; + font-weight: 600; +} + +.language-field .el-select, +.language-field .el-date-editor { + width: 100%; +} + +.language-cover { + min-height: 32px; + padding: 5px 10px; + display: flex; + align-items: center; + gap: 10px; + color: #94a3b8; + font-size: 12px; + background: #fff; + border: 1px solid #dcdfe6; + border-radius: 4px; +} + +.language-cover__upload { + padding: 5px 9px; + color: #fff; + font-size: 12px; + background: #409eff; + border: 0; + border-radius: 3px; + cursor: pointer; +} + +.language-cover__upload i { + margin-right: 3px; +} + +.language-cover__input { + flex: 1; + min-width: 0; +} + +.language-cover__name { + max-width: 120px; + overflow: hidden; + color: #16a34a; + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.language-richtext { + margin-top: 20px; +} + +.language-richtext .wang-editor-wrap { + height: 420px; + min-height: 420px; + padding-right: 0; +} + +.language-richtext .wang-toolbar { + border-radius: 0; +} + +.language-richtext__head { + padding: 12px 14px 8px; + display: flex; + align-items: center; + justify-content: space-between; + background: #fff; + border: 1px solid #edf0f5; + border-bottom: 0; + border-radius: 10px 10px 0 0; +} + +.language-richtext__head span { + color: #334155; + font-size: 13px; + font-weight: 600; +} + +.language-richtext__head small { + color: #94a3b8; + font-size: 11px; +} + +.language-richtext__toolbar { + min-height: 38px; + padding: 0 10px; + display: flex; + align-items: center; + gap: 5px; + color: #64748b; + background: #f8fafc; + border-top: 1px solid #edf2f7; + border-bottom: 1px solid #edf2f7; +} + +.language-richtext__toolbar > span { + min-width: 24px; + height: 24px; + padding: 0 5px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + border-radius: 4px; +} + +.language-richtext__toolbar > span:not(.language-richtext__divider):hover { + color: #2563eb; + background: #eff6ff; +} + +.language-richtext__strong { + font-weight: 700; +} + +.language-richtext__italic { + font-family: Georgia, serif; + font-style: italic; + font-weight: 700; +} + +.language-richtext__toolbar .language-richtext__divider { + min-width: 1px; + width: 1px; + height: 18px; + padding: 0; + margin: 0 4px; + background: #dbe2ea; +} + +.language-richtext__canvas { + min-height: 230px; + padding: 16px; + color: #c0c7d1; + font-size: 13px; + line-height: 1.8; + cursor: text; + outline: none; +} + +.language-richtext__canvas:empty::before { + content: attr(data-placeholder); + color: #c0c7d1; + pointer-events: none; +} + .article-editor__preview { flex: 0 0 auto; display: flex; diff --git a/f/web-kboss/src/views/homePage/news/NewsDetail.vue b/f/web-kboss/src/views/homePage/news/NewsDetail.vue index 0dfe457..119fdcf 100644 --- a/f/web-kboss/src/views/homePage/news/NewsDetail.vue +++ b/f/web-kboss/src/views/homePage/news/NewsDetail.vue @@ -53,6 +53,16 @@ export default { } } }, + computed: { + isEnglish() { + return this.$i18n && this.$i18n.locale === 'en-US' + } + }, + watch: { + isEnglish() { + this.getNewsDetail() + } + }, created() { this.getNewsDetail() }, @@ -84,6 +94,10 @@ export default { if (/^(https?:|blob:|data:|\/\/)/.test(url)) return url return `${window.location.origin}/idfile?path=${url}` }, + getLocalizedField(row, zhField, enField, legacyField) { + if (this.isEnglish && row[enField]) return row[enField] + return row[zhField] || row[legacyField] || row[enField] || '' + }, getResponseDetail(res) { if (res.data && res.data.data && !Array.isArray(res.data.data)) return res.data.data if (res.data && !Array.isArray(res.data)) return res.data @@ -94,12 +108,12 @@ export default { normalizeArticle(row) { return { id: row.id || this.$route.params.id, - title: row.title || '未命名文章', - summary: row.summary || '', - articleType: row.article_type || row.type || '企业动态', - publishTime: row.publish_time || row.publishTime || '', - coverImg: this.normalizeImageUrl(row.cover_img || row.coverImg || ''), - content: row.content || '' + title: this.getLocalizedField(row, 'title_zh', 'title_en', 'title') || '未命名文章', + summary: this.getLocalizedField(row, 'summary_zh', 'summary_en', 'summary'), + articleType: this.getLocalizedField(row, 'article_type_zh', 'article_type_en', 'article_type') || row.type || '企业动态', + publishTime: this.getLocalizedField(row, 'publish_time_zh', 'publish_time_en', 'publish_time') || row.publishTime || '', + coverImg: this.normalizeImageUrl(this.getLocalizedField(row, 'cover_img_zh', 'cover_img_en', 'cover_img') || row.coverImg || ''), + content: this.getLocalizedField(row, 'content_zh', 'content_en', 'content') } }, async getNewsDetail() { diff --git a/f/web-kboss/src/views/homePage/news/newsLog.vue b/f/web-kboss/src/views/homePage/news/newsLog.vue index caa094f..5cf9e37 100644 --- a/f/web-kboss/src/views/homePage/news/newsLog.vue +++ b/f/web-kboss/src/views/homePage/news/newsLog.vue @@ -291,16 +291,25 @@ export default { // 将接口字段统一转换成页面表格和编辑弹窗使用的字段。 return { id: row.id, - title: row.title || '', - summary: row.summary || '', - type: row.article_type || row.type || '企业动态', + title: row.title_zh || row.title || '', + summary: row.summary_zh || row.summary || '', + type: row.article_type_zh || row.article_type || row.type || '企业动态', status: this.getViewStatus(row.status), - publishTime: row.publish_time || row.publishTime || '', + publishTime: row.publish_time_zh || row.publish_time || row.publishTime || '', updateTime: row.update_time || row.updateTime || '', views: row.read_count || row.views || 0, - content: row.content || '', - coverUrl: row.cover_img || row.coverUrl || '', - coverName: row.cover_name || row.coverName || '' + content: row.content_zh || row.content || '', + coverUrl: row.cover_img_zh || row.cover_img || row.coverUrl || '', + coverName: row.cover_name_zh || row.cover_name || row.coverName || '', + english: { + title: row.title_en || '', + summary: row.summary_en || '', + category: row.article_type_en || '', + publishTime: row.publish_time_en || '', + content: row.content_en || '', + coverUrl: row.cover_img_en || '', + coverName: row.cover_name_en || '' + } } }, getResponseList(res) { @@ -325,17 +334,24 @@ export default { ) }, buildArticlePayload(article) { - // 将编辑弹窗字段转换成新增/编辑接口需要的字段。 + // 将中文、英文编辑字段转换成新增/编辑接口需要的双语字段。 + const english = article.english || {} return { ...(article.id ? { id: article.id } : {}), url_link: window.location.href, - title: article.title, - article_type: article.type, - summary: article.summary, + title_zh: article.title, + title_en: english.title || '', + article_type_zh: article.type, + article_type_en: english.category || '', + summary_zh: article.summary, + summary_en: english.summary || '', update_time: article.update_time || article.updateTime, - publish_time: article.publish_time || article.publishTime, - cover_img: article.coverUrl, - content: article.content, + publish_time_zh: article.publish_time_zh || article.publishTime, + publish_time_en: english.publishTime || '', + cover_img_zh: article.coverUrl, + cover_img_en: english.coverUrl || '', + content_zh: article.content, + content_en: english.content || '', status: this.getApiStatus(article.status) } }, @@ -726,4 +742,4 @@ export default { align-items: flex-start; } } - \ No newline at end of file + diff --git a/f/web-kboss/src/views/homePage/news/newsView.vue b/f/web-kboss/src/views/homePage/news/newsView.vue index 0f0cc2b..9c1d450 100644 --- a/f/web-kboss/src/views/homePage/news/newsView.vue +++ b/f/web-kboss/src/views/homePage/news/newsView.vue @@ -11,10 +11,9 @@
COMPANY NEWS
-

企业动态

- +

{{ isEnglish ? 'Company News' : '企业动态' }}

-

了解开元云最新动态,把握AI行业前沿资讯,与我们一起见证智能跃迁

+

{{ isEnglish ? 'Stay up to date with Open Computing AI, explore the latest AI industry insights, and witness the intelligent leap with us.' : '了解开元云最新动态,把握AI行业前沿资讯,与我们一起见证智能跃迁' }}

@@ -101,7 +100,7 @@ - + @@ -109,13 +108,12 @@