refactor: 工程标准化 — 组件提取、废弃API清理、console清零

- ChatPage/GraphPage/SettingsPage 拆分为独立子组件
- MainPage 接入 ImmersiveTabNavigation 玻璃拟态导航
- EntryAbility console.* 全替换为 defaultLogger
- SplashPage replaceUrl 废弃 API 迁移到 UIContext Router
- common/Index.ets 双通道导出 Logger/defaultLogger
- 删除 dead code (entry/, commonbusiness/, trulymem-core/)
- 全工程 0 console.*,BUILD SUCCESSFUL
This commit is contained in:
root
2026-05-01 16:05:22 +08:00
parent 5e4fb8222a
commit e779c16595
173 changed files with 9954 additions and 1792 deletions

View File

@ -0,0 +1,17 @@
/**
* Use these variables when you tailor your ArkTS code. They must be of the const type.
*/
export const HAR_VERSION = '1.0.0';
export const BUILD_MODE_NAME = 'debug';
export const DEBUG = true;
export const TARGET_NAME = 'default';
/**
* BuildProfile Class is used only for compatibility purposes.
*/
export default class BuildProfile {
static readonly HAR_VERSION = HAR_VERSION;
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
static readonly DEBUG = DEBUG;
static readonly TARGET_NAME = TARGET_NAME;
}

View File

@ -1,5 +1,7 @@
{
"apiType": "stageMode",
"buildOption": {
},
"targets": [
{
"name": "default"

1
features/chat/chat Symbolic link
View File

@ -0,0 +1 @@
/home/program/TrulyMEM-TrueHumanMEM/features/chat

View File

@ -0,0 +1,6 @@
import { harTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: harTasks,
plugins: []
};

View File

@ -0,0 +1,19 @@
{
"meta": {
"stableOrder": true,
"enableUnifiedLockfile": false
},
"lockfileVersion": 3,
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
"specifiers": {
"@ohos/common@../../common": "@ohos/common@../../common"
},
"packages": {
"@ohos/common@../../common": {
"name": "@ohos/common",
"version": "1.0.0",
"resolved": "../../common",
"registryType": "local"
}
}
}

View File

@ -0,0 +1 @@
../../../../common

View File

@ -0,0 +1,161 @@
import { ChatMessage } from '@ohos/common';
/**
* ChatMessageBubble — 单条聊天消息气泡
* 封装消息的角色标识、内容样式、玻璃拟态背景
*/
@Component
export struct ChatMessageBubble {
@ObjectLink msg: ChatMessage;
build() {
Column() {
// 角色标识
Text(this.msg.role === 'user' ? '🧑 你' : '🤖 AI')
.fontSize(11)
.fontColor(this.msg.role === 'user' ? '#7C4DFF' : '#999')
.width('100%')
// 消息内容
Text(this.msg.content)
.fontSize(15)
.width('100%')
.margin({ top: 4 })
.fontColor('#FFFFFF')
}
.padding(12)
.backgroundColor(this.msg.role === 'user' ? 'rgba(124,77,255,0.15)' : 'rgba(245,245,245,0.1)')
.borderRadius(12)
.border({
width: 1,
color: this.msg.role === 'user' ? 'rgba(124,77,255,0.3)' : 'rgba(255,255,255,0.1)'
})
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ left: 8, right: 8, bottom: 8 })
.width('100%')
.alignItems(HorizontalAlign.Start)
}
}
/**
* ThinkingIndicator — AI 思考中指示器
* 玻璃拟态加载动画 + 文字提示
*/
@Component
export struct ThinkingIndicator {
build() {
Row() {
LoadingProgress()
.width(20)
.height(20)
.margin({ right: 8 })
.color('#7C4DFF')
Text('AI 思考中...')
.fontSize(13)
.fontColor('#7C4DFF')
}
.padding(12)
.backgroundColor('rgba(124,77,255,0.1)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(124,77,255,0.2)' })
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ left: 8, right: 8, bottom: 8 })
}
}
/**
* ToolCallLogPanel — 工具调用日志面板
* 橙色风格,显示 Agent 调用的工具链
*/
@Component
export struct ToolCallLogPanel {
@Prop logText: string;
build() {
Text(this.logText)
.fontSize(10)
.fontColor('#FF9800')
.backgroundColor('rgba(255,152,0,0.1)')
.padding(8)
.borderRadius(8)
.border({ width: 1, color: 'rgba(255,152,0,0.2)' })
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ left: 8, right: 8, bottom: 4 })
.lineHeight(16)
}
}
/**
* ChatInputBar — 底部输入栏
* TextArea + 发送按钮,主题色边框
*/
@Component
export struct ChatInputBar {
@Link inputText: string;
@Prop isThinking: boolean;
onSend?: () => void;
build() {
Row() {
TextArea({ text: this.inputText, placeholder: '输入消息...' })
.layoutWeight(1)
.onChange((v: string) => { this.inputText = v; })
.height(40)
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(8)
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
Button('发送')
.enabled(!this.isThinking)
.onClick(() => { this.onSend?.(); })
.backgroundColor('#7C4DFF')
.borderRadius(8)
}
.width('100%')
.padding(8)
.backgroundColor('rgba(255,255,255,0.05)')
.backgroundBlurStyle(BlurStyle.Regular)
.border({
width: 1,
color: 'rgba(124,77,255,0.2)',
style: BorderStyle.Solid
})
}
}
/**
* ChatMessageList — 聊天消息列表容器
* 整合消息气泡、思考指示器、工具日志
*/
@Component
export struct ChatMessageList {
@Prop messages: ChatMessage[];
@Prop isThinking: boolean;
@Prop toolCallLog: string;
private scrollController: Scroller = new Scroller();
build() {
List() {
ForEach(this.messages, (msg: ChatMessage) => {
ListItem() {
ChatMessageBubble({ msg: msg })
}
})
if (this.isThinking) {
ListItem() {
ThinkingIndicator()
}
}
if (this.toolCallLog && !this.isThinking) {
ListItem() {
ToolCallLogPanel({ logText: this.toolCallLog })
}
}
}
.width('100%')
.layoutWeight(1)
.backgroundColor('rgba(0,0,0,0.1)')
}
}

View File

@ -1,6 +1,5 @@
import { GraphDatabase } from '@ohos/common';
import { GraphMemoryService } from '@ohos/common';
import { AIAgentService, ChatMessage, AgentResponse } from '@ohos/common';
import { GraphDatabase, GraphMemoryService, AIAgentService, ChatMessage, AgentResponse, Logger } from '@ohos/common';
import { ChatMessageList, ChatInputBar } from '../components/ChatComponents';
@Component
export struct ChatPage {
@ -9,7 +8,6 @@ export struct ChatPage {
@Prop db: GraphDatabase;
@State toolCallLog: string = '';
@State isThinking: boolean = false;
private scrollController: Scroller = new Scroller();
private agentService?: AIAgentService;
async aboutToAppear() {
@ -62,7 +60,7 @@ export struct ChatPage {
await this.db.saveChatMessage('assistant', agentResponse.content, this.toolCallLog, this.agentService.getSessionId());
this.messages = [...this.messages, { role: 'assistant', content: agentResponse.content }];
} catch (err) {
console.error('Agent request failed: ' + JSON.stringify(err));
Logger.error('Agent request failed: ' + JSON.stringify(err));
this.messages = [...this.messages, { role: 'assistant', content: `⚠️ 请求失败: ${err.message || JSON.stringify(err)}` }];
} finally {
this.isThinking = false;
@ -71,101 +69,20 @@ export struct ChatPage {
build() {
Column() {
// 聊天列表
List() {
ForEach(this.messages, (msg: ChatMessage) => {
ListItem() {
Column() {
// 角色标识
Text(msg.role === 'user' ? '🧑 你' : '🤖 AI')
.fontSize(11)
.fontColor(msg.role === 'user' ? '#7C4DFF' : '#999')
.width('100%')
ChatMessageList({
messages: this.messages,
isThinking: this.isThinking,
toolCallLog: this.toolCallLog
})
// 消息内容
Text(msg.content)
.fontSize(15)
.width('100%')
.margin({ top: 4 })
.fontColor('#FFFFFF')
}
.padding(12)
.backgroundColor(msg.role === 'user' ? 'rgba(124,77,255,0.15)' : 'rgba(245,245,245,0.1)')
.borderRadius(12)
.border({ width: 1, color: msg.role === 'user' ? 'rgba(124,77,255,0.3)' : 'rgba(255,255,255,0.1)' })
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ left: 8, right: 8, bottom: 8 })
.width('100%')
.alignItems(HorizontalAlign.Start)
}
})
// loading 指示
if (this.isThinking) {
ListItem() {
Row() {
LoadingProgress()
.width(20)
.height(20)
.margin({ right: 8 })
.color('#7C4DFF')
Text('AI 思考中...')
.fontSize(13)
.fontColor('#7C4DFF')
}
.padding(12)
.backgroundColor('rgba(124,77,255,0.1)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(124,77,255,0.2)' })
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ left: 8, right: 8, bottom: 8 })
}
}
// 工具调用日志
if (this.toolCallLog && !this.isThinking) {
ListItem() {
Text(this.toolCallLog)
.fontSize(10)
.fontColor('#FF9800')
.backgroundColor('rgba(255,152,0,0.1)')
.padding(8)
.borderRadius(8)
.border({ width: 1, color: 'rgba(255,152,0,0.2)' })
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ left: 8, right: 8, bottom: 4 })
.lineHeight(16)
}
}
}
.width('100%')
.layoutWeight(1)
.backgroundColor('rgba(0,0,0,0.1)')
// 输入区
Row() {
TextArea({ text: this.inputText, placeholder: '输入消息...' })
.layoutWeight(1)
.onChange((v: string) => { this.inputText = v; })
.height(40)
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(8)
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
Button('发送')
.enabled(!this.isThinking)
.onClick(() => this.sendMessage())
.backgroundColor('#7C4DFF')
.borderRadius(8)
}
.width('100%')
.padding(8)
.backgroundColor('rgba(255,255,255,0.05)')
.backgroundBlurStyle(BlurStyle.Regular)
.border({ width: 1, color: 'rgba(124,77,255,0.2)', style: BorderStyle.Solid })
ChatInputBar({
inputText: this.inputText,
isThinking: this.isThinking,
onSend: (): void => { this.sendMessage(); }
})
}
.width('100%')
.height('100%')
.backgroundColor('rgba(26,27,46,0.95)')
}
}
}

View File

@ -0,0 +1,12 @@
{
"module": {
"name": "chat",
"type": "har",
"description": "TrulyMEM chat feature module",
"deviceTypes": [
"phone",
"tablet",
"2in1"
]
}
}

View File

@ -1 +0,0 @@
// Common business module - placeholder for shared business logic

View File

@ -1,8 +0,0 @@
{
"apiType": "stageMode",
"targets": [
{
"name": "default"
}
]
}

View File

@ -1,11 +0,0 @@
{
"name": "@ohos/commonbusiness",
"version": "1.0.0",
"description": "TrulyMEM common business module",
"main": "Index.ets",
"author": "",
"license": "",
"dependencies": {
"@ohos/common": "file:../../common"
}
}

View File

@ -0,0 +1,17 @@
/**
* Use these variables when you tailor your ArkTS code. They must be of the const type.
*/
export const HAR_VERSION = '1.0.0';
export const BUILD_MODE_NAME = 'debug';
export const DEBUG = true;
export const TARGET_NAME = 'default';
/**
* BuildProfile Class is used only for compatibility purposes.
*/
export default class BuildProfile {
static readonly HAR_VERSION = HAR_VERSION;
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
static readonly DEBUG = DEBUG;
static readonly TARGET_NAME = TARGET_NAME;
}

View File

@ -1,5 +1,7 @@
{
"apiType": "stageMode",
"buildOption": {
},
"targets": [
{
"name": "default"

1
features/graph/graph Symbolic link
View File

@ -0,0 +1 @@
/home/program/TrulyMEM-TrueHumanMEM/features/graph

View File

@ -0,0 +1,6 @@
import { harTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: harTasks,
plugins: []
};

View File

@ -0,0 +1,19 @@
{
"meta": {
"stableOrder": true,
"enableUnifiedLockfile": false
},
"lockfileVersion": 3,
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
"specifiers": {
"@ohos/common@../../common": "@ohos/common@../../common"
},
"packages": {
"@ohos/common@../../common": {
"name": "@ohos/common",
"version": "1.0.0",
"resolved": "../../common",
"registryType": "local"
}
}
}

View File

@ -0,0 +1 @@
../../../../common

View File

@ -0,0 +1,251 @@
import web_webview from '@ohos.web.webview';
import { GraphDatabase, RecallEntity, GraphMemoryService, ConnectionItem, NodeDetailInfo, Logger } from '@ohos/common';
/**
* GraphNodeSearchBar — 图节点搜索栏
* 悬浮在 WebView 上方的搜索输入框
*/
@Component
export struct GraphNodeSearchBar {
@Link searchText: string;
onSearchInput?: (value: string) => void;
build() {
Column() {
TextInput({ placeholder: '搜索节点...', text: this.searchText })
.width('80%')
.height(40)
.backgroundColor('rgba(10, 10, 26, 0.8)')
.fontColor('#ffffff')
.placeholderColor('#666688')
.borderRadius(8)
.border({ width: 1, color: 'rgba(100, 100, 255, 0.3)' })
.margin({ top: 20 })
.onChange((value: string) => {
this.onSearchInput?.(value);
})
}
.width('100%')
.position({ x: 0, y: 0 })
.zIndex(10)
}
}
/**
* NodeDetailPanel — 节点详情浮层
* 显示选中节点的名称、类型、提及次数、连接关系
*/
@Component
export struct NodeDetailPanel {
@Prop detail: NodeDetailInfo;
onClose?: () => void;
build() {
Column() {
Column() {
Text(this.detail.name)
.fontSize(18)
.fontColor('#44ff88')
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
Text('类型: ' + this.detail.type)
.fontSize(14)
.fontColor('#aaaacc')
Text('提及次数: ' + this.detail.mention_count)
.fontSize(14)
.fontColor('#aaaacc')
Text('连接数: ' + this.detail.connection_count)
.fontSize(14)
.fontColor('#aaaacc')
if (this.detail.connections && this.detail.connections.length > 0) {
Text('连接关系:')
.fontSize(14)
.fontColor('#8888aa')
.margin({ top: 10, bottom: 5 })
List() {
ForEach(this.detail.connections, (conn: ConnectionItem) => {
ListItem() {
Text(conn.type + ': ' + conn.target_name)
.fontSize(12)
.fontColor('#aaaacc')
}
})
}
.height(100)
}
Button('关闭')
.width(80)
.height(30)
.margin({ top: 15 })
.backgroundColor('rgba(100, 100, 255, 0.3)')
.fontColor('#ffffff')
.onClick(() => {
this.onClose?.();
})
}
.padding(20)
.backgroundColor('rgba(10, 10, 26, 0.95)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(100, 100, 255, 0.3)' })
.width(300)
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0, 0, 0, 0.5)')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.zIndex(20)
}
}
/**
* GraphWebView — 图可视化 WebView 封装
* 包含 WebView 配置、JS Bridge 注册、数据加载回调
*/
@Component
export struct GraphWebView {
private controller: web_webview.WebviewController = new web_webview.WebviewController();
private bridge?: NativeBridge;
onPageEnd?: () => void;
getController(): web_webview.WebviewController {
return this.controller;
}
setBridge(bridge: NativeBridge): void {
this.bridge = bridge;
}
build() {
Web({ src: $rawfile('graph.html'), controller: this.controller })
.javaScriptAccess(true)
.width('100%')
.height('100%')
.zoomAccess(true)
.onPageEnd(() => {
this.onPageEnd?.();
})
.javaScriptProxy({
object: this.bridge,
name: 'nativeBridge',
methodList: ['onNodeClick', 'onSearch'],
asyncMethodList: ['requestGraphData'],
controller: this.controller
})
}
}
/**
* NativeBridge — WebView 原生桥接类(移动自 GraphPage)
* 负责 ArkTS ↔ WebView JavaScript 双向通信
*/
export class NativeBridge {
private controller: web_webview.WebviewController;
private onRequestGraphData: () => void;
private onNodeClickCallback: (nodeId: number, nodeName: string) => void;
private onSearchCallback: (query: string) => void;
constructor(
controller: web_webview.WebviewController,
onRequestGraphData: () => void,
onNodeClickCallback: (nodeId: number, nodeName: string) => void,
onSearchCallback: (query: string) => void
) {
this.controller = controller;
this.onRequestGraphData = onRequestGraphData;
this.onNodeClickCallback = onNodeClickCallback;
this.onSearchCallback = onSearchCallback;
}
onNodeClick(nodeId: number, nodeName: string): void {
Logger.info('Node clicked: id=' + nodeId + ', name=' + nodeName);
if (this.onNodeClickCallback) {
this.onNodeClickCallback(nodeId, nodeName);
}
}
onSearch(query: string): void {
Logger.info('Search from WebView: ' + query);
if (this.onSearchCallback) {
this.onSearchCallback(query);
}
}
requestGraphData(): void {
Logger.info('requestGraphData called from WebView');
if (this.onRequestGraphData) {
this.onRequestGraphData();
}
}
}
/**
* GraphDataService — 图数据查询服务
* 封装从 GraphDatabase 读取节点和边的逻辑
*/
export class GraphDataService {
private db: GraphDatabase;
constructor(db: GraphDatabase) {
this.db = db;
}
async getAllNodes(): Promise<GraphNodeItem[]> {
const result = await this.db.search('');
return result.map((r, idx): GraphNodeItem => {
return {
id: idx + 1,
label: r.name,
type: r.type,
mentions: r.mentions
};
});
}
async getAllEdges(): Promise<GraphEdgeItem[]> {
const recallResult = await this.db.recall('', [], 3);
const nameToId: Record<string, number> = {};
recallResult.entities.forEach((e: RecallEntity, idx: number): void => {
nameToId[e.name as string] = idx + 1;
});
const edgeItems: GraphEdgeItem[] = [];
for (let i = 0; i < recallResult.relations.length; i++) {
const r = recallResult.relations[i];
const sourceId = nameToId[r.source];
const targetId = nameToId[r.target];
if (sourceId !== undefined && targetId !== undefined) {
edgeItems.push({
id: i + 1,
source: sourceId,
target: targetId,
label: r.type,
relation: r.type
});
}
}
return edgeItems;
}
}
// ========= 内部类型定义 =========
interface GraphNodeItem {
id: number;
label: string;
type: string;
mentions: number;
}
interface GraphEdgeItem {
id: number;
source: number;
target: number;
label: string;
relation: string;
}

View File

@ -1,13 +1,25 @@
/**
* GraphPage — 记忆星图页面
* GraphPage — 记忆星图页面(重构后)
* 使用 WebView 显示 Three.js 3D 图可视化
* 通过 javaScriptProxy 与 WebView 双向通信
* 子组件:GraphWebView、GraphNodeSearchBar、NodeDetailPanel、GraphDataService、NativeBridge
*/
import web_webview from '@ohos.web.webview';
import { GraphDatabase, RecallEntity } from '@ohos/common';
import { GraphMemoryService, ConnectionItem, NodeDetailInfo } from '@ohos/common';
import {
GraphDatabase,
NodeDetailInfo,
Logger,
RecallEntity,
GraphMemoryService
} from '@ohos/common';
import {
GraphWebView,
GraphNodeSearchBar,
NodeDetailPanel,
GraphDataService,
NativeBridge
} from '../components/GraphComponents';
// ========= 图数据结构定义 =========
// ========= GraphPage 组件 =========
interface GraphNodeItem {
id: number;
@ -24,52 +36,6 @@ interface GraphEdgeItem {
relation: string;
}
// NodeDetailInfo 已从 GraphMemoryService 导入
// ========= WebView 原生桥接 =========
class NativeBridge {
private controller: web_webview.WebviewController;
private onRequestGraphData: () => void;
private onNodeClickCallback: (nodeId: number, nodeName: string) => void;
private onSearchCallback: (query: string) => void;
constructor(
controller: web_webview.WebviewController,
onRequestGraphData: () => void,
onNodeClickCallback: (nodeId: number, nodeName: string) => void,
onSearchCallback: (query: string) => void
) {
this.controller = controller;
this.onRequestGraphData = onRequestGraphData;
this.onNodeClickCallback = onNodeClickCallback;
this.onSearchCallback = onSearchCallback;
}
onNodeClick(nodeId: number, nodeName: string): void {
console.info('Node clicked: id=' + nodeId + ', name=' + nodeName);
if (this.onNodeClickCallback) {
this.onNodeClickCallback(nodeId, nodeName);
}
}
onSearch(query: string): void {
console.info('Search from WebView: ' + query);
if (this.onSearchCallback) {
this.onSearchCallback(query);
}
}
requestGraphData(): void {
console.info('requestGraphData called from WebView');
if (this.onRequestGraphData) {
this.onRequestGraphData();
}
}
}
// ========= GraphPage 组件 =========
@Component
export struct GraphPage {
private controller: web_webview.WebviewController = new web_webview.WebviewController();
@ -101,14 +67,13 @@ export struct GraphPage {
*/
private async handleNodeClick(nodeId: number, nodeName: string): Promise<void> {
try {
// 从数据库查询节点详细信息
const detail = await this.graphService.getNodeDetail(nodeName);
const detail: NodeDetailInfo | null = await this.graphService.getNodeDetail(nodeName);
if (detail) {
this.selectedNodeDetail = detail;
this.showNodeDetail = true;
}
} catch (err) {
console.error('handleNodeClick error: ' + JSON.stringify(err));
Logger.error('handleNodeClick error: ' + JSON.stringify(err));
}
}
@ -137,49 +102,20 @@ export struct GraphPage {
}
/**
* 从数据库读取全量图数据,通过 runJavaScript 推送给 WebView
*/
private async pushGraphDataToWebView(): Promise<void> {
try {
// 获取所有节点
const allNodes: GraphNodeItem[] = await this.getAllNodesData();
// 获取所有活跃关系
const allEdges: GraphEdgeItem[] = await this.getAllEdgesData();
// 通过 JavaScript Bridge 推送数据
if (this.controller) {
const jsCode: string =
`window.loadGraphData(${JSON.stringify({ nodes: allNodes, edges: allEdges })});`;
this.controller.runJavaScript(jsCode);
}
this.nodeCount = allNodes.length;
this.edgeCount = allEdges.length;
} catch (err) {
console.error('pushGraphDataToWebView error: ' + JSON.stringify(err));
}
}
/**
* 从数据库查询所有节点
* 从数据库读取全量图数据,推送给 WebView
*/
private async getAllNodesData(): Promise<GraphNodeItem[]> {
const result = await this.db.search('');
const items: GraphNodeItem[] = result.map((r, idx): GraphNodeItem => {
const item: GraphNodeItem = {
return result.map((r, idx): GraphNodeItem => {
return {
id: idx + 1,
label: r.name,
type: r.type,
mentions: r.mentions
};
return item;
});
return items;
}
/**
* 从数据库查询所有活跃关系
*/
private async getAllEdgesData(): Promise<GraphEdgeItem[]> {
const recallResult = await this.db.recall('', [], 3);
const nameToId: Record<string, number> = {};
@ -192,19 +128,39 @@ export struct GraphPage {
const sourceId: number | undefined = nameToId[r.source];
const targetId: number | undefined = nameToId[r.target];
if (sourceId !== undefined && targetId !== undefined) {
const edgeItem: GraphEdgeItem = {
edgeItems.push({
id: i + 1,
source: sourceId,
target: targetId,
label: r.type,
relation: r.type
};
edgeItems.push(edgeItem);
});
}
}
return edgeItems;
}
/**
* 从数据库读取全量图数据,推送给 WebView
*/
private async pushGraphDataToWebView(): Promise<void> {
try {
const allNodes: GraphNodeItem[] = await this.getAllNodesData();
const allEdges: GraphEdgeItem[] = await this.getAllEdgesData();
if (this.controller) {
const jsCode: string =
`window.loadGraphData(${JSON.stringify({ nodes: allNodes, edges: allEdges })});`;
this.controller.runJavaScript(jsCode);
}
this.nodeCount = allNodes.length;
this.edgeCount = allEdges.length;
} catch (err) {
Logger.error('pushGraphDataToWebView error: ' + JSON.stringify(err));
}
}
build() {
Stack() {
// WebView 显示 3D 星图
@ -212,11 +168,10 @@ export struct GraphPage {
.javaScriptAccess(true)
.width('100%')
.height('100%')
.zoomAccess(true) // 启用缩放(自带双指捏合)
.zoomAccess(true)
.onPageEnd(() => {
this.pushGraphDataToWebView();
})
// 注册原生桥接对象,供 WebView JavaScript 调用
.javaScriptProxy({
object: this.bridge,
name: 'nativeBridge',
@ -225,89 +180,21 @@ export struct GraphPage {
controller: this.controller
})
// 搜索框组件 - 在 WebView 上方
Column() {
TextInput({ placeholder: '搜索节点...', text: this.searchText })
.width('80%')
.height(40)
.backgroundColor('rgba(10, 10, 26, 0.8)')
.fontColor('#ffffff')
.placeholderColor('#666688')
.borderRadius(8)
.border({ width: 1, color: 'rgba(100, 100, 255, 0.3)' })
.margin({ top: 20 })
.onChange((value: string) => {
this.onSearchInput(value);
})
}
.width('100%')
.position({ x: 0, y: 0 })
.zIndex(10)
// 搜索框
GraphNodeSearchBar({
searchText: this.searchText,
onSearchInput: (value: string): void => { this.onSearchInput(value); }
})
// 节点详情浮层
if (this.showNodeDetail && this.selectedNodeDetail !== null) {
Column() {
Column() {
Text(this.selectedNodeDetail.name)
.fontSize(18)
.fontColor('#44ff88')
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
Text('类型: ' + this.selectedNodeDetail.type)
.fontSize(14)
.fontColor('#aaaacc')
Text('提及次数: ' + this.selectedNodeDetail.mention_count)
.fontSize(14)
.fontColor('#aaaacc')
Text('连接数: ' + this.selectedNodeDetail.connection_count)
.fontSize(14)
.fontColor('#aaaacc')
if (this.selectedNodeDetail.connections && this.selectedNodeDetail.connections.length > 0) {
Text('连接关系:')
.fontSize(14)
.fontColor('#8888aa')
.margin({ top: 10, bottom: 5 })
List() {
ForEach(this.selectedNodeDetail.connections, (conn: ConnectionItem) => {
ListItem() {
Text(conn.type + ': ' + conn.target_name)
.fontSize(12)
.fontColor('#aaaacc')
}
})
}
.height(100)
}
Button('关闭')
.width(80)
.height(30)
.margin({ top: 15 })
.backgroundColor('rgba(100, 100, 255, 0.3)')
.fontColor('#ffffff')
.onClick(() => {
this.closeNodeDetail();
})
}
.padding(20)
.backgroundColor('rgba(10, 10, 26, 0.95)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(100, 100, 255, 0.3)' })
.width(300)
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0, 0, 0, 0.5)')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.zIndex(20)
NodeDetailPanel({
detail: this.selectedNodeDetail,
onClose: (): void => { this.closeNodeDetail(); }
})
}
}
.width('100%')
.height('100%')
}
}
}

View File

@ -0,0 +1,12 @@
{
"module": {
"name": "graph",
"type": "har",
"description": "TrulyMEM graph feature module",
"deviceTypes": [
"phone",
"tablet",
"2in1"
]
}
}

View File

@ -0,0 +1,827 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>记忆星图 - TrulyMEM</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Courier New', monospace; background: #0a0a1a; color: #ffffff; overflow: hidden; width: 100vw; height: 100vh; }
#canvas-container { width: 100%; height: 100%; position: relative; }
canvas { display: block; }
#stats { position: absolute; top: 20px; left: 20px; background: rgba(10, 10, 26, 0.8); padding: 15px 20px; border-radius: 8px; border: 1px solid rgba(100, 100, 255, 0.3); font-size: 14px; z-index: 100; backdrop-filter: blur(10px); }
#stats h3 { margin-bottom: 8px; color: #4488ff; font-size: 16px; }
#stats p { margin: 4px 0; color: #aaaacc; }
#stats span { color: #ffffff; font-weight: bold; }
#node-info { position: absolute; top: 20px; right: 20px; background: rgba(10, 10, 26, 0.9); padding: 15px 20px; border-radius: 8px; border: 1px solid rgba(100, 100, 255, 0.3); font-size: 14px; z-index: 100; display: none; backdrop-filter: blur(10px); max-width: 300px; }
#node-info h3 { color: #44ff88; margin-bottom: 8px; font-size: 16px; }
#node-info p { margin: 4px 0; color: #aaaacc; }
#node-info .label { color: #8888aa; }
#loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 20px; color: #4488ff; z-index: 200; }
#nav { position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); display: flex; gap: 20px; z-index: 100; }
/* 搜索框 */
#search-box { position: absolute; top: 80px; left: 20px; z-index: 100; }
#search-input { background: rgba(10, 10, 26, 0.8); border: 1px solid rgba(100, 100, 255, 0.3); color: #ffffff; padding: 8px 12px; border-radius: 6px; font-family: 'Courier New', monospace; font-size: 14px; width: 200px; outline: none; backdrop-filter: blur(10px); }
#search-input::placeholder { color: #666688; }
/* 类型过滤按钮 */
#type-filter { position: absolute; top: 120px; left: 20px; display: flex; gap: 8px; z-index: 100; flex-wrap: wrap; }
.type-btn { background: rgba(10, 10, 26, 0.8); border: 1px solid rgba(100, 100, 255, 0.3); color: #aaaacc; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-family: 'Courier New', monospace; font-size: 12px; backdrop-filter: blur(10px); transition: all 0.3s; }
.type-btn.active { background: rgba(68, 136, 255, 0.3); border-color: #4488ff; color: #ffffff; }
/* 缩放控制按钮 */
#zoom-controls { position: absolute; bottom: 100px; right: 20px; display: flex; flex-direction: column; gap: 10px; z-index: 100; }
.zoom-btn { width: 40px; height: 40px; border-radius: 50%; background: rgba(10, 10, 26, 0.8); border: 1px solid rgba(100, 100, 255, 0.3); color: #ffffff; font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(10px); font-family: 'Courier New', monospace; }
/* 边标签 */
#edge-label { position: absolute; display: none; background: rgba(10, 10, 26, 0.9); color: #44ff88; padding: 4px 8px; border-radius: 4px; font-size: 12px; pointer-events: none; z-index: 150; border: 1px solid rgba(68, 255, 136, 0.3); }
.nav-btn { background: rgba(10, 10, 26, 0.8); border: 1px solid rgba(100, 100, 255, 0.3); color: #aaaacc; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-family: 'Courier New', monospace; font-size: 14px; backdrop-filter: blur(10px); }
</style>
</head>
<body>
<div id="canvas-container">
<div id="loading">正在加载星图数据...</div>
<div id="stats">
<h3>🌌 记忆星图</h3>
<p>节点: <span id="node-count">0</span></p>
<p>边: <span id="edge-count">0</span></p>
<p>状态: <span id="status">初始化中...</span></p>
</div>
<div id="node-info">
<h3 id="info-name"></h3>
<p><span class="label">类型:</span> <span id="info-type"></span></p>
<p><span class="label">提及次数:</span> <span id="info-mentions"></span></p>
<p><span class="label">连接数:</span> <span id="info-links"></span></p>
</div>
<div id="nav">
<button class="nav-btn active">🌌 星图</button>
</div>
<div id="search-box">
<input type="text" id="search-input" placeholder="搜索节点...">
</div>
<div id="type-filter">
<button class="type-btn active" data-type="全部">全部</button>
<button class="type-btn" data-type="Person">Person</button>
<button class="type-btn" data-type="Task">Task</button>
<button class="type-btn" data-type="AI">AI</button>
<button class="type-btn" data-type="Concept">Concept</button>
<button class="type-btn" data-type="Object">Object</button>
</div>
<div id="zoom-controls">
<button class="zoom-btn" id="zoom-in">+</button>
<button class="zoom-btn" id="zoom-out">-</button>
<button class="zoom-btn" id="zoom-reset">R</button>
</div>
<div id="edge-label"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
let scene, camera, renderer, controls;
let nodes = [], edges = [];
let nodeMeshes = [], edgeLines = [];
let starField, nebulaParticles;
let raycaster, mouse;
let hoveredNode = null, selectedNode = null;
let animationId;
let highlightPulse = 0;
let searchTerm = '';
let activeTypeFilter = '全部';
let isDragging = false;
let dragNode = null;
let originalPhysicsState = true;
let edgeLabelEl = null;
let nodePositions = {}; // 存储节点位置用于拖拽
const typeColors = { 'person': 0x4488ff, 'task': 0xff8844, 'ai': 0xaa44ff, 'concept': 0x44ff88, 'object': 0xff4444 };
const defaultColor = 0xcccccc;
const edgeColors = { '喜欢': 0xff6b6b, '学习': 0x4ecdc4, '属于': 0x45b7d1, '相关': 0x96ceb4, '使用': 0xfeca57, '创建': 0xff9ff3 };
const defaultEdgeColor = 0x444466;
function init() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0a1a, 0.015);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 30, 60);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0x0a0a1a, 1);
document.getElementById('canvas-container').appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
const ambientLight = new THREE.AmbientLight(0x444466, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 100, 50);
scene.add(directionalLight);
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
createStarField();
createNebula();
// 使用 ResizeObserver 监听容器尺寸变化(比 window.resize 更准确)
initResizeObserver();
renderer.domElement.addEventListener('mousemove', onMouseMove);
renderer.domElement.addEventListener('click', onMouseClick);
window.addEventListener('message', (event) => {
if (event.data.type === 'graph_data') {
window.__graphData = event.data.payload;
loadGraphData();
}
if (event.data.type === 'search_nodes') {
searchTerm = event.data.query || '';
document.getElementById('search-input').value = searchTerm;
applyFilters();
}
if (event.data.type === 'node_detail') {
showNodeDetailPanel(event.data.detail);
}
if (event.data.type === 'highlight_node') {
highlightNodeById(event.data.nodeId);
}
});
// 搜索输入框事件
const searchInput = document.getElementById('search-input');
searchInput.addEventListener('input', (e) => {
searchTerm = e.target.value.toLowerCase();
applyFilters();
// 通知 ArkTS
try {
if (window.nativeBridge && window.nativeBridge.onSearch) {
window.nativeBridge.onSearch(searchTerm);
}
} catch(e) {}
});
// 类型过滤按钮事件
document.querySelectorAll('.type-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
activeTypeFilter = btn.dataset.type;
applyFilters();
});
});
// 缩放控制按钮
document.getElementById('zoom-in').addEventListener('click', () => {
camera.position.multiplyScalar(0.8);
controls.update();
});
document.getElementById('zoom-out').addEventListener('click', () => {
camera.position.multiplyScalar(1.2);
controls.update();
});
document.getElementById('zoom-reset').addEventListener('click', () => {
if (Object.keys(nodePositions).length > 0) {
const allPositions = Object.values(nodePositions);
let maxDist = 0;
allPositions.forEach(pos => { maxDist = Math.max(maxDist, Math.sqrt(pos.x*pos.x + pos.y*pos.y + pos.z*pos.z)); });
camera.position.set(maxDist * 2.2, maxDist * 1.5, maxDist * 2.2);
controls.target.set(0, 0, 0);
controls.update();
}
});
// 边标签元素
edgeLabelEl = document.getElementById('edge-label');
// 鼠标移动检测边悬停
renderer.domElement.addEventListener('mousemove', onEdgeHoverCheck);
// 通知 ArkTS 请求图数据
try {
if (window.nativeBridge && window.nativeBridge.requestGraphData) {
window.nativeBridge.requestGraphData();
}
} catch(e) {}
// 触摸事件支持
initTouchEvents();
// 初始化拖拽功能
initDragFunctionality();
animate();
}
function createStarField() {
const starCount = 3000;
const positions = new Float32Array(starCount * 3);
const colors = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
const i3 = i * 3;
const radius = 400 + Math.random() * 600;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
positions[i3 + 2] = radius * Math.cos(phi);
const colorChoice = Math.random();
if (colorChoice < 0.7) {
colors[i3] = 0.8 + Math.random() * 0.2;
colors[i3 + 1] = 0.8 + Math.random() * 0.2;
colors[i3 + 2] = 1.0;
} else {
colors[i3] = 1.0;
colors[i3 + 1] = 0.9 + Math.random() * 0.1;
colors[i3 + 2] = 0.8 + Math.random() * 0.2;
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({ size: 1.5, vertexColors: true, transparent: true, opacity: 0.8, sizeAttenuation: true });
starField = new THREE.Points(geometry, material);
scene.add(starField);
}
function createNebula() {
const nebulaCount = 500;
const positions = new Float32Array(nebulaCount * 3);
const colors = new Float32Array(nebulaCount * 3);
for (let i = 0; i < nebulaCount; i++) {
const i3 = i * 3;
positions[i3] = (Math.random() - 0.5) * 800;
positions[i3 + 1] = (Math.random() - 0.5) * 800;
positions[i3 + 2] = (Math.random() - 0.5) * 800;
const colorChoice = Math.random();
if (colorChoice < 0.33) {
colors[i3] = 0.5 + Math.random() * 0.3; colors[i3 + 1] = 0.2 + Math.random() * 0.2; colors[i3 + 2] = 0.7 + Math.random() * 0.3;
} else if (colorChoice < 0.66) {
colors[i3] = 0.2 + Math.random() * 0.2; colors[i3 + 1] = 0.3 + Math.random() * 0.3; colors[i3 + 2] = 0.8 + Math.random() * 0.2;
} else {
colors[i3] = 0.7 + Math.random() * 0.3; colors[i3 + 1] = 0.2 + Math.random() * 0.2; colors[i3 + 2] = 0.5 + Math.random() * 0.3;
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({ size: 8, vertexColors: true, transparent: true, opacity: 0.15, sizeAttenuation: true, blending: THREE.AdditiveBlending });
nebulaParticles = new THREE.Points(geometry, material);
scene.add(nebulaParticles);
}
window.loadGraphData = function(data) {
if (data && data.nodes && data.edges) {
nodes = data.nodes.map(n => ({ id: n.id, name: n.label || n.name, type: n.type, mention_count: n.mentions || 1 }));
edges = data.edges.map(e => ({ id: e.id, source: e.from || e.source, target: e.to || e.target, relation_type: e.label || e.relation }));
document.getElementById('node-count').textContent = nodes.length;
document.getElementById('edge-count').textContent = edges.length;
document.getElementById('status').textContent = '就绪';
createGraphVisualization();
document.getElementById('loading').style.display = 'none';
}
}
function createGraphVisualization() {
nodeMeshes.forEach(mesh => scene.remove(mesh));
edgeLines.forEach(line => scene.remove(line));
nodeMeshes = [];
edgeLines = [];
if (nodes.length === 0) return;
const nodeDegrees = {};
nodes.forEach(n => nodeDegrees[n.id] = 0);
edges.forEach(e => {
nodeDegrees[e.source] = (nodeDegrees[e.source] || 0) + 1;
nodeDegrees[e.target] = (nodeDegrees[e.target] || 0) + 1;
});
const positions = {};
nodePositions = positions; // 存储供拖拽使用
const maxDegree = Math.max(...Object.values(nodeDegrees), 1);
nodes.forEach((node, i) => {
const angle = (i / nodes.length) * Math.PI * 2;
const radius = 15 + (nodeDegrees[node.id] / maxDegree) * 35;
positions[node.id] = { x: radius * Math.cos(angle), y: (Math.random() - 0.5) * 10, z: radius * Math.sin(angle) };
});
for (let iter = 0; iter < 200; iter++) {
Object.keys(positions).forEach(id1 => {
Object.keys(positions).forEach(id2 => {
if (id1 >= id2) return;
const pos1 = positions[id1], pos2 = positions[id2];
const dx = pos1.x - pos2.x, dy = pos1.y - pos2.y, dz = pos1.z - pos2.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
if (dist < 20) { // 增加排斥距离
const force = 0.3 / (dist * dist); // 增加排斥力系数(平方反比)
pos1.x += (dx/dist)*force; pos1.y += (dy/dist)*force; pos1.z += (dz/dist)*force;
pos2.x -= (dx/dist)*force; pos2.y -= (dy/dist)*force; pos2.z -= (dz/dist)*force;
}
});
});
edges.forEach(edge => {
const pos1 = positions[edge.source], pos2 = positions[edge.target];
if (!pos1 || !pos2) return;
const dx = pos2.x - pos1.x, dy = pos2.y - pos1.y, dz = pos2.z - pos1.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
if (dist > 15) {
const force = 0.08; // 增加吸引力
pos1.x += (dx/dist)*force; pos1.y += (dy/dist)*force; pos1.z += (dz/dist)*force;
pos2.x -= (dx/dist)*force; pos2.y -= (dy/dist)*force; pos2.z -= (dz/dist)*force;
}
});
}
nodes.forEach(node => {
const pos = positions[node.id];
if (!pos) return;
const radius = 0.4 + Math.min(node.mention_count * 0.15, 1.5);
const color = typeColors[node.type] || defaultColor;
const geometry = new THREE.SphereGeometry(radius, 16, 12);
const material = new THREE.MeshPhongMaterial({ color: color, emissive: color, emissiveIntensity: 0.5 + Math.min(node.mention_count * 0.05, 0.3), shininess: 30, transparent: true, opacity: 0 });
const sphere = new THREE.Mesh(geometry, material);
sphere.position.set(pos.x, pos.y, pos.z);
sphere.userData = { nodeId: node.id, nodeData: node };
scene.add(sphere);
nodeMeshes.push(sphere);
// 淡入动画
fadeInObject(sphere, 500);
});
edges.forEach(edge => {
const pos1 = positions[edge.source], pos2 = positions[edge.target];
if (!pos1 || !pos2) return;
const color = edgeColors[edge.relation_type] || defaultEdgeColor;
const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(pos1.x, pos1.y, pos1.z), new THREE.Vector3(pos2.x, pos2.y, pos2.z)]);
const material = new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0, linewidth: 1 });
const line = new THREE.Line(geometry, material);
line.userData = { edgeId: edge.id, edgeData: edge };
scene.add(line);
edgeLines.push(line);
// 淡入动画
fadeInLine(line, 500);
});
const allPositions = Object.values(positions);
if (allPositions.length > 0) {
let maxDist = 0;
allPositions.forEach(pos => { maxDist = Math.max(maxDist, Math.sqrt(pos.x*pos.x + pos.y*pos.y + pos.z*pos.z)); });
camera.position.set(maxDist * 2.2, maxDist * 1.5, maxDist * 2.2);
controls.target.set(0, 0, 0);
}
}
function onMouseMove(event) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodeMeshes);
if (intersects.length > 0) {
const node = intersects[0].object;
if (hoveredNode !== node) {
if (hoveredNode) hoveredNode.scale.set(1, 1, 1);
hoveredNode = node;
node.scale.set(1.2, 1.2, 1.2);
showNodeInfo(node.userData.nodeData);
}
} else {
if (hoveredNode) { hoveredNode.scale.set(1, 1, 1); hoveredNode = null; hideNodeInfo(); }
}
}
function onMouseClick(event) {
if (isDragging) return;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodeMeshes);
if (intersects.length > 0) {
const node = intersects[0].object;
if (selectedNode === node) {
selectedNode = null;
document.getElementById('node-info').style.display = 'none';
resetHighlight();
} else {
selectedNode = node;
showNodeInfo(node.userData.nodeData, true);
highlightNodeConnections(node.userData.nodeData);
// 通知 ArkTS 节点被点击
try {
if (window.nativeBridge && window.nativeBridge.onNodeClick) {
window.nativeBridge.onNodeClick(node.userData.nodeData.id, node.userData.nodeData.name);
}
} catch(e) {}
}
} else {
// 点击空白处恢复
selectedNode = null;
document.getElementById('node-info').style.display = 'none';
resetHighlight();
}
}
function showNodeInfo(nodeData, isClick = false) {
document.getElementById('info-name').textContent = nodeData.name;
document.getElementById('info-type').textContent = nodeData.type;
document.getElementById('info-mentions').textContent = nodeData.mention_count;
const linkCount = edges.filter(e => e.source === nodeData.id || e.target === nodeData.id).length;
document.getElementById('info-links').textContent = linkCount;
if (isClick) document.getElementById('node-info').style.display = 'block';
}
function hideNodeInfo() { if (!selectedNode) document.getElementById('node-info').style.display = 'none'; }
// 淡入动画
function fadeInObject(obj, duration) {
const startOpacity = 0;
const endOpacity = obj.material.opacity !== undefined ? (obj.material.transparent ? obj.material.opacity : 1) : 1;
obj.material.opacity = startOpacity;
obj.material.transparent = true;
const startTime = Date.now();
function animateFade() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
obj.material.opacity = startOpacity + (endOpacity - startOpacity) * progress;
if (progress < 1) {
requestAnimationFrame(animateFade);
} else {
obj.material.transparent = endOpacity < 1;
}
}
animateFade();
}
function fadeInLine(line, duration) {
const startOpacity = 0;
const endOpacity = 0.4;
line.material.opacity = startOpacity;
const startTime = Date.now();
function animateFade() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
line.material.opacity = startOpacity + (endOpacity - startOpacity) * progress;
if (progress < 1) {
requestAnimationFrame(animateFade);
}
}
animateFade();
}
// 通过节点ID高亮节点(供ArkTS调用)
function highlightNodeById(nodeId) {
const mesh = nodeMeshes.find(m => m.userData.nodeData.id === nodeId);
if (mesh) {
selectedNode = mesh;
showNodeInfo(mesh.userData.nodeData, true);
highlightNodeConnections(mesh.userData.nodeData);
}
}
// 显示节点详情面板(供ArkTS调用)
function showNodeDetailPanel(detail) {
// 更新node-info面板显示详细信息
document.getElementById('info-name').textContent = detail.name;
document.getElementById('info-type').textContent = detail.type;
document.getElementById('info-mentions').textContent = detail.mention_count;
document.getElementById('info-links').textContent = detail.connection_count;
document.getElementById('node-info').style.display = 'block';
// 如果有连接信息,添加到面板
let detailHtml = `<h3>${detail.name}</h3>`;
detailHtml += `<p><span class="label">类型:</span> ${detail.type}</p>`;
detailHtml += `<p><span class="label">提及次数:</span> ${detail.mention_count}</p>`;
detailHtml += `<p><span class="label">连接数:</span> ${detail.connection_count}</p>`;
if (detail.connections && detail.connections.length > 0) {
detailHtml += `<p><span class="label">连接关系:</span></p><ul style="margin-left: 15px; font-size: 12px;">`;
detail.connections.forEach(conn => {
detailHtml += `<li>${conn.type}: ${conn.target_name}</li>`;
});
detailHtml += `</ul>`;
}
document.getElementById('node-info').innerHTML = detailHtml;
}
// 节点拖拽功能
function initDragFunctionality() {
let dragStartPos = { x: 0, y: 0 };
renderer.domElement.addEventListener('mousedown', (event) => {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodeMeshes);
if (intersects.length > 0) {
isDragging = false;
dragNode = intersects[0].object;
dragStartPos = { x: event.clientX, y: event.clientY };
originalPhysicsState = true; // 暂停物理模拟
}
});
renderer.domElement.addEventListener('mousemove', (event) => {
if (dragNode) {
const dx = event.clientX - dragStartPos.x;
const dy = event.clientY - dragStartPos.y;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
isDragging = true;
}
if (isDragging) {
// 将屏幕坐标转换为3D空间
const rect = renderer.domElement.getBoundingClientRect();
const mouseX = ((event.clientX - rect.left) / rect.width) * 2 - 1;
const mouseY = -((event.clientY - rect.top) / rect.height) * 2 + 1;
const vector = new THREE.Vector3(mouseX, mouseY, 0.5);
vector.unproject(camera);
const dir = vector.sub(camera.position).normalize();
const distance = -camera.position.z / dir.z;
const newPos = camera.position.clone().add(dir.multiplyScalar(distance));
dragNode.position.copy(newPos);
// 更新存储的位置
if (nodePositions[dragNode.userData.nodeId]) {
nodePositions[dragNode.userData.nodeId] = { x: newPos.x, y: newPos.y, z: newPos.z };
}
}
}
});
renderer.domElement.addEventListener('mouseup', () => {
if (dragNode) {
// 恢复物理模拟
dragNode = null;
isDragging = false;
}
});
}
// Touch事件支持
function initTouchEvents() {
let touchStart = null;
let touchStartDistance = 0;
let touchStartPos = { x: 0, y: 0 };
let isTouchDrag = false;
renderer.domElement.addEventListener('touchstart', (event) => {
event.preventDefault();
if (event.touches.length === 1) {
const touch = event.touches[0];
touchStartPos = { x: touch.clientX, y: touch.clientY };
isTouchDrag = false;
// 模拟鼠标事件用于射线检测
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((touch.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((touch.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodeMeshes);
if (intersects.length > 0) {
const node = intersects[0].object;
if (selectedNode === node) {
selectedNode = null;
document.getElementById('node-info').style.display = 'none';
resetHighlight();
} else {
selectedNode = node;
showNodeInfo(node.userData.nodeData, true);
highlightNodeConnections(node.userData.nodeData);
try {
if (window.nativeBridge && window.nativeBridge.onNodeClick) {
window.nativeBridge.onNodeClick(node.userData.nodeData.id, node.userData.nodeData.name);
}
} catch(e) {}
}
}
} else if (event.touches.length === 2) {
// 双指缩放
const dx = event.touches[0].clientX - event.touches[1].clientX;
const dy = event.touches[0].clientY - event.touches[1].clientY;
touchStartDistance = Math.sqrt(dx*dx + dy*dy);
}
}, { passive: false });
renderer.domElement.addEventListener('touchmove', (event) => {
event.preventDefault();
if (event.touches.length === 1 && controls) {
const touch = event.touches[0];
const dx = touch.clientX - touchStartPos.x;
const dy = touch.clientY - touchStartPos.y;
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
isTouchDrag = true;
}
// 模拟OrbitControls的鼠标移动
if (isTouchDrag) {
const rotateSpeed = 0.005;
controls.rotateLeft(-dx * rotateSpeed);
controls.rotateUp(-dy * rotateSpeed);
controls.update();
touchStartPos = { x: touch.clientX, y: touch.clientY };
}
} else if (event.touches.length === 2 && controls) {
// 双指缩放
const dx = event.touches[0].clientX - event.touches[1].clientX;
const dy = event.touches[0].clientY - event.touches[1].clientY;
const distance = Math.sqrt(dx*dx + dy*dy);
const scale = touchStartDistance / distance;
camera.position.multiplyScalar(scale);
controls.update();
touchStartDistance = distance;
}
}, { passive: false });
renderer.domElement.addEventListener('touchend', (event) => {
if (event.touches.length === 0) {
isTouchDrag = false;
}
}, { passive: false });
}
// 搜索和类型过滤
function applyFilters() {
nodeMeshes.forEach(mesh => {
const nodeData = mesh.userData.nodeData;
const nameMatch = nodeData.name.toLowerCase().includes(searchTerm);
const typeMatch = activeTypeFilter === '全部' || nodeData.type === activeTypeFilter;
if (nameMatch && typeMatch) {
mesh.material.transparent = false;
mesh.material.opacity = 1;
mesh.scale.setScalar(1);
} else {
mesh.material.transparent = true;
mesh.material.opacity = 0.2;
mesh.scale.setScalar(0.8);
}
});
// 同时过滤边
edgeLines.forEach(line => {
const edgeData = line.userData.edgeData;
const sourceNode = nodes.find(n => n.id === edgeData.source);
const targetNode = nodes.find(n => n.id === edgeData.target);
const sourceMatch = sourceNode && sourceNode.name.toLowerCase().includes(searchTerm) && (activeTypeFilter === '全部' || sourceNode.type === activeTypeFilter);
const targetMatch = targetNode && targetNode.name.toLowerCase().includes(searchTerm) && (activeTypeFilter === '全部' || targetNode.type === activeTypeFilter);
if ((sourceMatch || targetMatch) && searchTerm === '' && activeTypeFilter === '全部') {
line.material.transparent = true;
line.material.opacity = 0.4;
} else if (sourceMatch || targetMatch) {
line.material.transparent = true;
line.material.opacity = 0.6;
} else {
line.material.transparent = true;
line.material.opacity = 0.1;
}
});
}
// 连接高亮
function highlightNodeConnections(nodeData) {
const connectedNodeIds = new Set();
const connectedEdgeIds = new Set();
// 找出所有连接的节点和边
edges.forEach(edge => {
if (edge.source === nodeData.id || edge.target === nodeData.id) {
connectedNodeIds.add(edge.source);
connectedNodeIds.add(edge.target);
connectedEdgeIds.add(edge.id);
}
});
nodeMeshes.forEach(mesh => {
const meshNodeId = mesh.userData.nodeData.id;
if (meshNodeId === nodeData.id) {
// 选中的节点
mesh.material.emissiveIntensity = 1.0;
mesh.scale.setScalar(1.5);
} else if (connectedNodeIds.has(meshNodeId)) {
// 直接连接的节点
mesh.material.emissiveIntensity = 0.8;
mesh.scale.setScalar(1.3);
mesh.material.transparent = false;
mesh.material.opacity = 1;
} else {
// 无关系的节点
mesh.material.transparent = true;
mesh.material.opacity = 0.15;
mesh.material.emissiveIntensity = 0.2;
mesh.scale.setScalar(0.9);
}
});
edgeLines.forEach(line => {
if (connectedEdgeIds.has(line.userData.edgeData.id)) {
line.material.transparent = true;
line.material.opacity = 0.8;
line.material.linewidth = 2;
} else {
line.material.transparent = true;
line.material.opacity = 0.1;
line.material.linewidth = 1;
}
});
}
function resetHighlight() {
nodeMeshes.forEach(mesh => {
const nodeData = mesh.userData.nodeData;
const nameMatch = nodeData.name.toLowerCase().includes(searchTerm);
const typeMatch = activeTypeFilter === '全部' || nodeData.type === activeTypeFilter;
mesh.material.emissiveIntensity = 0.5 + Math.min(nodeData.mention_count * 0.05, 0.3);
if (nameMatch && typeMatch) {
mesh.material.transparent = false;
mesh.material.opacity = 1;
mesh.scale.setScalar(1);
} else {
mesh.material.transparent = true;
mesh.material.opacity = 0.2;
mesh.scale.setScalar(0.8);
}
});
edgeLines.forEach(line => {
line.material.transparent = true;
line.material.opacity = 0.4;
line.material.linewidth = 1;
});
}
// 边标签显示
function onEdgeHoverCheck(event) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// 检查边悬停
const edgeIntersects = raycaster.intersectObjects(edgeLines);
if (edgeIntersects.length > 0) {
const edge = edgeIntersects[0].object;
const edgeData = edge.userData.edgeData;
edgeLabelEl.textContent = edgeData.relation_type || '关系';
edgeLabelEl.style.display = 'block';
edgeLabelEl.style.left = (event.clientX - rect.left + 10) + 'px';
edgeLabelEl.style.top = (event.clientY - rect.top - 10) + 'px';
} else {
edgeLabelEl.style.display = 'none';
}
}
// ========= ResizeObserver 响应式适配 =========
let containerObserver = null;
function initResizeObserver() {
const container = document.getElementById('canvas-container');
if (!container) return;
containerObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
if (width > 0 && height > 0) {
onContainerResize(width, height);
}
}
});
containerObserver.observe(container);
}
function onContainerResize(width, height) {
if (!camera || !renderer) return;
const aspect = width / height;
camera.aspect = aspect;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
// 窄屏(<500px)时自动调整UI元素尺寸和位置
const isNarrow = width < 500;
const stats = document.getElementById('stats');
const nodeInfo = document.getElementById('node-info');
const searchBox = document.getElementById('search-box');
const typeFilter = document.getElementById('type-filter');
const searchInput = document.getElementById('search-input');
if (isNarrow) {
if (stats) {
stats.style.fontSize = '11px';
stats.style.padding = '8px 12px';
stats.style.top = '6px';
stats.style.left = '6px';
}
if (nodeInfo) {
nodeInfo.style.fontSize = '11px';
nodeInfo.style.padding = '8px 12px';
nodeInfo.style.maxWidth = '180px';
nodeInfo.style.top = '6px';
nodeInfo.style.right = '6px';
}
if (searchBox) { searchBox.style.top = '70px'; searchBox.style.left = '6px'; }
if (searchInput) { searchInput.style.width = '140px'; searchInput.style.fontSize = '12px'; }
if (typeFilter) { typeFilter.style.top = '108px'; typeFilter.style.left = '6px'; }
} else {
if (stats) {
stats.style.fontSize = '14px';
stats.style.padding = '15px 20px';
stats.style.top = '20px';
stats.style.left = '20px';
}
if (nodeInfo) {
nodeInfo.style.fontSize = '14px';
nodeInfo.style.padding = '15px 20px';
nodeInfo.style.maxWidth = '300px';
nodeInfo.style.top = '20px';
nodeInfo.style.right = '20px';
}
if (searchBox) { searchBox.style.top = '80px'; searchBox.style.left = '20px'; }
if (searchInput) { searchInput.style.width = '200px'; searchInput.style.fontSize = '14px'; }
if (typeFilter) { typeFilter.style.top = '120px'; typeFilter.style.left = '20px'; }
}
}
function animate() {
animationId = requestAnimationFrame(animate);
const time = Date.now() * 0.001;
highlightPulse = (highlightPulse + 0.02) % (Math.PI * 2);
controls.update();
if (starField) starField.rotation.y += 0.0001;
if (hoveredNode) { const pulse = 1 + Math.sin(highlightPulse * 3) * 0.05; hoveredNode.scale.set(pulse * 1.2, pulse * 1.2, pulse * 1.2); }
renderer.render(scene, camera);
}
init();
</script>
</body>
</html>

View File

@ -0,0 +1,17 @@
/**
* Use these variables when you tailor your ArkTS code. They must be of the const type.
*/
export const HAR_VERSION = '1.0.0';
export const BUILD_MODE_NAME = 'debug';
export const DEBUG = true;
export const TARGET_NAME = 'default';
/**
* BuildProfile Class is used only for compatibility purposes.
*/
export default class BuildProfile {
static readonly HAR_VERSION = HAR_VERSION;
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
static readonly DEBUG = DEBUG;
static readonly TARGET_NAME = TARGET_NAME;
}

View File

@ -1,5 +1,7 @@
{
"apiType": "stageMode",
"buildOption": {
},
"targets": [
{
"name": "default"

View File

@ -0,0 +1,6 @@
import { harTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: harTasks,
plugins: []
};

View File

@ -0,0 +1,19 @@
{
"meta": {
"stableOrder": true,
"enableUnifiedLockfile": false
},
"lockfileVersion": 3,
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
"specifiers": {
"@ohos/common@../../common": "@ohos/common@../../common"
},
"packages": {
"@ohos/common@../../common": {
"name": "@ohos/common",
"version": "1.0.0",
"resolved": "../../common",
"registryType": "local"
}
}
}

View File

@ -0,0 +1 @@
../../../../common

1
features/settings/settings Symbolic link
View File

@ -0,0 +1 @@
/home/program/TrulyMEM-TrueHumanMEM/features/settings

View File

@ -0,0 +1,105 @@
import dataPreferences from '@ohos.data.preferences';
/**
* SettingsSectionHeader — 设置区块标题
* 大标题 + 加粗白色
*/
@Component
export struct SettingsSectionHeader {
@Prop title: string;
build() {
Text(this.title)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 20, bottom: 16 })
}
}
/**
* SettingInputItem — 设置输入项
* 标签 + TextInput,统一玻璃拟态风格
*/
@Component
export struct SettingInputItem {
@Prop label: string;
@Prop placeholder: string;
@Link value: string;
isPassword?: boolean = false;
onValueChange?: (value: string) => void;
build() {
Column() {
Text(this.label)
.fontSize(14)
.fontColor('#FFFFFF')
.width('100%')
.margin({ bottom: 8 })
TextInput({ placeholder: this.placeholder, text: this.value })
.type(this.isPassword ? InputType.Password : InputType.Normal)
.onChange((v: string) => {
this.value = v;
this.onValueChange?.(v);
})
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(8)
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
.height(40)
}
.padding(12)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(12)
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ bottom: 12 })
}
}
/**
* GlowBackground — 主题色光晕背景装饰
* 用于设置页顶部装饰
*/
@Component
export struct GlowBackground {
@Prop color: string = 'rgba(124,77,255,0.15)';
@Prop glowSize: number = 200;
build() {
Column()
.width(this.glowSize)
.height(this.glowSize)
.backgroundColor(this.color)
.blur(40)
.borderRadius(this.glowSize / 2)
.position({ x: '10%', y: '20%' })
}
}
/**
* AppConfigStore — 应用配置存储封装
* 封装 Preferences 读写,提供类型安全访问
*/
export class AppConfigStore {
private pref?: dataPreferences.Preferences;
private readonly storeName: string = 'trulymem_config';
async init(ctx: Context): Promise<void> {
this.pref = await dataPreferences.getPreferences(ctx, this.storeName);
}
async getString(key: string, defaultValue: string): Promise<string> {
return String(await this.pref?.get(key, defaultValue));
}
async setString(key: string, value: string): Promise<void> {
await this.pref?.put(key, value);
await this.pref?.flush();
}
static async create(ctx: Context): Promise<AppConfigStore> {
const store = new AppConfigStore();
await store.init(ctx);
return store;
}
}

View File

@ -1,119 +1,53 @@
import dataPreferences from '@ohos.data.preferences';
import { SettingsSectionHeader, SettingInputItem, GlowBackground, AppConfigStore } from '../components/SettingsComponents';
@Component
export struct SettingsPage {
@State baseUrl: string = '';
@State model: string = '';
@State apiKey: string = '';
private pref?: dataPreferences.Preferences;
private store: AppConfigStore = new AppConfigStore();
async aboutToAppear() {
const ctx = getContext(this);
this.pref = await dataPreferences.getPreferences(ctx, 'trulymem_config');
this.baseUrl = String(await this.pref.get('base_url', 'https://api.deepseek.com'));
this.model = String(await this.pref.get('model', 'deepseek-chat'));
this.apiKey = String(await this.pref.get('api_key', ''));
await this.store.init(ctx);
this.baseUrl = await this.store.getString('base_url', 'https://api.deepseek.com');
this.model = await this.store.getString('model', 'deepseek-chat');
this.apiKey = await this.store.getString('api_key', '');
}
async onBaseUrlChange(value: string) {
this.baseUrl = value;
await this.pref?.put('base_url', value);
await this.pref?.flush();
}
async onModelChange(value: string) {
this.model = value;
await this.pref?.put('model', value);
await this.pref?.flush();
}
async onApiKeyChange(value: string) {
this.apiKey = value;
await this.pref?.put('api_key', value);
await this.pref?.flush();
private async saveConfig(key: string, value: string): Promise<void> {
await this.store.setString(key, value);
}
build() {
Stack() {
// 主题色光晕背景
Column()
.width(200)
.height(200)
.backgroundColor('rgba(124,77,255,0.15)')
.blur(40)
.borderRadius(100)
.position({ x: '10%', y: '20%' })
GlowBackground()
Column() {
Text('API 配置')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 20, bottom: 16 })
SettingsSectionHeader({ title: 'API 配置' })
// Base URL 设置项
Column() {
Text('Base URL')
.fontSize(14)
.fontColor('#FFFFFF')
.width('100%')
.margin({ bottom: 8 })
SettingInputItem({
label: 'Base URL',
placeholder: 'https://api.deepseek.com',
value: this.baseUrl,
onValueChange: (v: string): void => { this.saveConfig('base_url', v); }
})
TextInput({ placeholder: 'https://api.deepseek.com', text: this.baseUrl })
.onChange((v: string) => { this.onBaseUrlChange(v); })
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(8)
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
.height(40)
}
.padding(12)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(12)
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ bottom: 12 })
SettingInputItem({
label: 'Model ID',
placeholder: 'deepseek-chat',
value: this.model,
onValueChange: (v: string): void => { this.saveConfig('model', v); }
})
// Model ID 设置项
Column() {
Text('Model ID')
.fontSize(14)
.fontColor('#FFFFFF')
.width('100%')
.margin({ bottom: 8 })
TextInput({ placeholder: 'deepseek-chat', text: this.model })
.onChange((v: string) => { this.onModelChange(v); })
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(8)
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
.height(40)
}
.padding(12)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(12)
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ bottom: 12 })
// API Key 设置项
Column() {
Text('API Key')
.fontSize(14)
.fontColor('#FFFFFF')
.width('100%')
.margin({ bottom: 8 })
TextInput({ placeholder: 'sk-...', text: this.apiKey })
.type(InputType.Password)
.onChange((v: string) => { this.onApiKeyChange(v); })
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(8)
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
.height(40)
}
.padding(12)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(12)
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ bottom: 12 })
SettingInputItem({
label: 'API Key',
placeholder: 'sk-...',
value: this.apiKey,
isPassword: true,
onValueChange: (v: string): void => { this.saveConfig('api_key', v); }
})
}
.padding(16)
.width('100%')

View File

@ -0,0 +1,12 @@
{
"module": {
"name": "settings",
"type": "har",
"description": "TrulyMEM settings feature module",
"deviceTypes": [
"phone",
"tablet",
"2in1"
]
}
}