feat: 修复 Redis 超时问题、固定小程序端口、新增人生事件模块及优化多个页面

- 修复 Redis 超时:添加 commons-pool2 依赖,启用 Lettuce 连接池,超时提升至 15s
- 固定 mini-program H5 端口为 5175,避免与 web 项目端口冲突
- 新增人生事件(life-event)模块:表单和详情页面
- 新增 EpicScript 灵感接口(Controller/Service/DTO)
- 优化登录、引导、主页、记录、剧本详情等多个页面
- 优化服务管理脚本和 Nginx 配置

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 11:38:35 +08:00
parent 507d1ebdab
commit 60c63850ee
36 changed files with 4545 additions and 3043 deletions
+1
View File
@@ -374,3 +374,4 @@ check-https.sh
comparison-report.png
prototype-*.png
OpenClaw*.md
.gstack/
@@ -0,0 +1,3 @@
{"type":"click","text":"一个\n \n 完整原型通过\n 重建所有八个原型屏幕,并在同一工作流中添加后端灵感端点/额外的 DTO。","choice":"a","id":null,"timestamp":1778377847647}
{"type":"click","text":"一个\n \n 完整原型通过\n 重建所有八个原型屏幕,并在同一工作流中添加后端灵感端点/额外的 DTO。","choice":"a","id":null,"timestamp":1778377848524}
{"type":"click","text":"B\n \n 前端优先原型匹配\n 重建小程序 UI 以匹配原型,同时保留现有 API;缺失的灵感功能使用本地建议和现有的 createScript。","choice":"b","id":null,"timestamp":1778377848920}
@@ -0,0 +1 @@
{"reason":"idle timeout","timestamp":1778258237598}
+6
View File
@@ -111,6 +111,12 @@
<version>2.0.40</version>
</dependency>
<!-- Redis Lettuce 连接池支持 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
@@ -3,9 +3,12 @@ package com.emotion.controller;
import com.emotion.common.PageResult;
import com.emotion.common.Result;
import com.emotion.dto.request.EpicScriptCreateRequest;
import com.emotion.dto.request.EpicScriptInspirationRequest;
import com.emotion.dto.request.EpicScriptPageRequest;
import com.emotion.dto.request.EpicScriptUpdateRequest;
import com.emotion.dto.response.EpicScriptInspirationResponse;
import com.emotion.dto.response.EpicScriptResponse;
import com.emotion.dto.response.InspirationSuggestionResponse;
import com.emotion.service.EpicScriptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
@@ -45,6 +48,32 @@ public class EpicScriptController {
return Result.success(scripts);
}
@GetMapping(value = "/inspiration/recommendations")
public Result<List<InspirationSuggestionResponse>> getInspirationRecommendations() {
return Result.success(epicScriptService.getInspirationRecommendations());
}
@GetMapping(value = "/inspiration/random")
public Result<List<InspirationSuggestionResponse>> getRandomInspirations(
@RequestParam(required = false, defaultValue = "3") Integer size) {
return Result.success(epicScriptService.getRandomInspirations(size));
}
@PostMapping(value = "/inspiration/generate")
public Result<EpicScriptInspirationResponse> generateFromInspiration(
@Valid @RequestBody EpicScriptInspirationRequest request) {
EpicScriptInspirationResponse response;
try {
response = epicScriptService.generateFromInspiration(request);
} catch (IllegalStateException e) {
return Result.error(e.getMessage());
}
if (response == null) {
return Result.error("灵感剧本生成失败");
}
return Result.success(response);
}
/**
* 根据ID获取爽文剧本详情
*/
@@ -0,0 +1,28 @@
package com.emotion.dto.request;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
@Data
@EqualsAndHashCode(callSuper = true)
public class EpicScriptInspirationRequest extends BaseRequest {
@NotBlank(message = "灵感内容不能为空")
@Size(max = 500, message = "灵感内容不能超过500个字符")
private String prompt;
private String mode = "inspiration";
private String style;
private String length;
private String characterInfo;
private String lifeEventsSummary;
private String source;
}
@@ -0,0 +1,17 @@
package com.emotion.dto.response;
import lombok.Data;
import java.util.List;
@Data
public class EpicScriptInspirationResponse {
private EpicScriptResponse script;
private String prompt;
private Integer remainingCount;
private List<InspirationSuggestionResponse> suggestions;
}
@@ -0,0 +1,17 @@
package com.emotion.dto.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class InspirationSuggestionResponse {
private String text;
private String tag;
private String category;
}
@@ -3,9 +3,12 @@ package com.emotion.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.emotion.common.PageResult;
import com.emotion.dto.request.EpicScriptCreateRequest;
import com.emotion.dto.request.EpicScriptInspirationRequest;
import com.emotion.dto.request.EpicScriptPageRequest;
import com.emotion.dto.request.EpicScriptUpdateRequest;
import com.emotion.dto.response.EpicScriptInspirationResponse;
import com.emotion.dto.response.EpicScriptResponse;
import com.emotion.dto.response.InspirationSuggestionResponse;
import com.emotion.entity.EpicScript;
import java.util.List;
@@ -49,6 +52,12 @@ public interface EpicScriptService extends IService<EpicScript> {
*/
EpicScriptResponse createScript(EpicScriptCreateRequest request);
List<InspirationSuggestionResponse> getInspirationRecommendations();
List<InspirationSuggestionResponse> getRandomInspirations(Integer size);
EpicScriptInspirationResponse generateFromInspiration(EpicScriptInspirationRequest request);
/**
* 更新剧本
*
@@ -5,9 +5,12 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.EpicScriptCreateRequest;
import com.emotion.dto.request.EpicScriptInspirationRequest;
import com.emotion.dto.request.EpicScriptPageRequest;
import com.emotion.dto.request.EpicScriptUpdateRequest;
import com.emotion.dto.response.EpicScriptInspirationResponse;
import com.emotion.dto.response.EpicScriptResponse;
import com.emotion.dto.response.InspirationSuggestionResponse;
import com.emotion.entity.EpicScript;
import com.emotion.mapper.EpicScriptMapper;
import com.emotion.service.AiChatService;
@@ -21,7 +24,12 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -38,6 +46,17 @@ public class EpicScriptServiceImpl extends ServiceImpl<EpicScriptMapper, EpicScr
implements EpicScriptService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final int DAILY_INSPIRATION_LIMIT = 3;
private static final List<InspirationSuggestionResponse> INSPIRATION_SUGGESTIONS = List.of(
new InspirationSuggestionResponse("我想把最近一次低谷,改写成主角觉醒的开端。", "觉醒", "转折"),
new InspirationSuggestionResponse("如果我在最遗憾的选择里勇敢了一次,人生会怎样展开?", "遗憾", "重启"),
new InspirationSuggestionResponse("把一次普通的职场挑战,写成逆风翻盘的高光篇章。", "职场", "成长"),
new InspirationSuggestionResponse("我想见到十年后的自己,让 TA 给现在的我一封信。", "未来", "对话"),
new InspirationSuggestionResponse("把一段关系里的告别,写成重新认识自己的旅程。", "关系", "治愈"),
new InspirationSuggestionResponse("让我的童年记忆成为故事里的隐藏力量。", "童年", "力量"),
new InspirationSuggestionResponse("把一次失败的面试、考试或竞赛,改写成命运伏笔。", "挑战", "伏笔"),
new InspirationSuggestionResponse("写一个我终于不再讨好别人,开始选择自己的平行人生。", "自我", "选择")
);
/**
* Coze工作流配置键 - 爽文剧本生成
@@ -167,6 +186,76 @@ public class EpicScriptServiceImpl extends ServiceImpl<EpicScriptMapper, EpicScr
return convertToResponse(script);
}
@Override
public List<InspirationSuggestionResponse> getInspirationRecommendations() {
return INSPIRATION_SUGGESTIONS;
}
@Override
public List<InspirationSuggestionResponse> getRandomInspirations(Integer size) {
int limit = size == null ? 3 : Math.max(1, Math.min(size, INSPIRATION_SUGGESTIONS.size()));
List<InspirationSuggestionResponse> suggestions = new ArrayList<>(INSPIRATION_SUGGESTIONS);
Collections.shuffle(suggestions);
return suggestions.subList(0, limit);
}
@Override
public EpicScriptInspirationResponse generateFromInspiration(EpicScriptInspirationRequest request) {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return null;
}
int usedToday = countTodayScripts(currentUserId);
if (usedToday >= DAILY_INSPIRATION_LIMIT) {
throw new IllegalStateException("今日灵感生成次数已用完");
}
String prompt = request.getPrompt().trim();
EpicScriptCreateRequest createRequest = new EpicScriptCreateRequest();
createRequest.setTitle(buildInspirationTitle(prompt));
createRequest.setTheme(prompt);
createRequest.setStyle(StringUtils.hasText(request.getStyle()) ? request.getStyle() : "career");
createRequest.setLength(StringUtils.hasText(request.getLength()) ? request.getLength() : "medium");
createRequest.setCharacterInfo(request.getCharacterInfo());
createRequest.setLifeEventsSummary(request.getLifeEventsSummary());
Map<String, Object> plotJson = new HashMap<>();
plotJson.put("mode", "inspiration");
plotJson.put("prompt", prompt);
plotJson.put("source", StringUtils.hasText(request.getSource()) ? request.getSource() : "mini-program");
createRequest.setPlotJson(plotJson);
EpicScriptResponse script = createScript(createRequest);
EpicScriptInspirationResponse response = new EpicScriptInspirationResponse();
response.setScript(script);
response.setPrompt(prompt);
response.setRemainingCount(Math.max(0, DAILY_INSPIRATION_LIMIT - usedToday - 1));
response.setSuggestions(getRandomInspirations(3));
return response;
}
private int countTodayScripts(String userId) {
LocalDate today = LocalDate.now();
LocalDateTime start = today.atStartOfDay();
LocalDateTime end = today.plusDays(1).atStartOfDay();
LambdaQueryWrapper<EpicScript> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EpicScript::getUserId, userId)
.eq(EpicScript::getIsDeleted, 0)
.ge(EpicScript::getCreateTime, start)
.lt(EpicScript::getCreateTime, end);
return Math.toIntExact(this.count(wrapper));
}
private String buildInspirationTitle(String prompt) {
String normalized = prompt.replaceAll("\\s+", " ").trim();
if (normalized.length() <= 22) {
return normalized;
}
return normalized.substring(0, 22) + "...";
}
/**
* 调用Coze AI生成爽文剧本内容
*
@@ -15,27 +15,28 @@ spring:
minimum-idle: 10
maximum-pool-size: 50
auto-commit: true
idle-timeout: 600000
idle-timeout: 120000
pool-name: EmotionHikariCP-Local
max-lifetime: 1800000
connection-timeout: 30000
max-lifetime: 300000
keepalive-time: 60000
connection-timeout: 10000
validation-timeout: 5000
leak-detection-threshold: 60000
# Redis配置 - 与prod一致
# Redis配置 - 远程Redis,增加超时和连接池优化
redis:
host: 101.200.208.45
port: 6379
timeout: 5000ms
timeout: 15000ms
database: 0
password: EmotionMuseum2025*#
lettuce:
pool:
max-active: 20
max-wait: -1ms
max-wait: 10000ms
max-idle: 10
min-idle: 5
time-between-eviction-runs: 30s
shutdown-timeout: 100ms
client-name: emotion-museum-client
# 日志配置 - 本地开发详细日志
@@ -15,10 +15,11 @@ spring:
minimum-idle: 10
maximum-pool-size: 50
auto-commit: true
idle-timeout: 600000
idle-timeout: 120000
pool-name: EmotionHikariCP-Prod
max-lifetime: 1800000
connection-timeout: 30000
max-lifetime: 300000
keepalive-time: 60000
connection-timeout: 10000
validation-timeout: 5000
leak-detection-threshold: 60000
+7 -4
View File
@@ -13,7 +13,7 @@ server {
# HTTPS 服务器
server {
listen 443 ssl http2;
listen 443 ssl;
server_name lifescript.happylifeos.com;
# SSL 证书配置
@@ -22,10 +22,13 @@ server {
# SSL 优化配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# WeChat mini program/Cronet compatibility: keep TLS 1.2 enabled with broad modern suites.
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_ecdh_curve prime256v1:secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HSTS (可选,生产环境建议开启)
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+158 -10
View File
@@ -49,11 +49,17 @@ LOG_DIR.mkdir(exist_ok=True)
SCRIPT_LOG = LOG_DIR / "dev-services.log"
MAX_SCAN_DEPTH = 3
SKIP_DIRS = {"node_modules", "__pycache__", ".git", "venv", ".venv", "target", "build", "dist", ".omc", ".next", ".nuxt", "coverage", "e2e"}
SKIP_DIRS = {
"node_modules", "__pycache__", ".git", "venv", ".venv", "target", "build",
"dist", ".omc", ".next", ".nuxt", "coverage", "e2e",
}
AUXILIARY_DIRS = {"tools", "scripts", "script", "bin", ".superpowers", ".gstack"}
# 框架默认端口
DEFAULT_PORTS = {
"vite": 5173,
"uniapp": 5173,
"taro": 5173,
"next": 3000,
"node-backend": 3000,
"python": 8000,
@@ -134,6 +140,8 @@ class Service:
def type_label(self) -> str:
labels = {
"vite": "Vite 前端",
"uniapp": "UniApp H5",
"taro": "Taro H5",
"next": "Next.js",
"node-backend": "Node.js 后端",
"python": "Python 后端",
@@ -162,7 +170,7 @@ def _scan_directory(current: Path, root: Path, max_depth: int, services: list):
return
# 跳过黑名单
if current.name in SKIP_DIRS:
if current != root and (current.name in SKIP_DIRS or current.name in AUXILIARY_DIRS):
return
# 尝试发现服务
@@ -206,6 +214,10 @@ def _detect_service(directory: Path) -> Optional[Service]:
deps = {**pkg_data.get("dependencies", {}), **pkg_data.get("devDependencies", {})}
scripts = pkg_data.get("scripts", {})
if _is_uniapp_project(deps, scripts):
return _build_uniapp_service(directory, pkg_data)
if _is_taro_project(deps, scripts):
return _build_taro_service(directory, pkg_data)
# Vite
if "vite" in deps:
return _build_vite_service(directory, pkg_data)
@@ -263,7 +275,7 @@ def _read_env_file(directory: Path) -> dict:
def _extract_port_from_env(directory: Path) -> Optional[int]:
"""从 .env 文件中提取端口号"""
env_data = _read_env_file(directory)
for key in ["VITE_PORT", "PORT", "SERVER_PORT", "APP_PORT", "BACKEND_PORT"]:
for key in ["VITE_PORT", "VITE_APP_PORT", "PORT", "SERVER_PORT", "APP_PORT", "BACKEND_PORT"]:
if key in env_data:
try:
return int(env_data[key])
@@ -291,13 +303,61 @@ def _extract_port_from_vite_config(directory: Path) -> Optional[int]:
def _extract_port_from_scripts(pkg_data: dict) -> Optional[int]:
"""从 package.json scripts 中提取 --port 参数"""
scripts = pkg_data.get("scripts", {})
dev_script = scripts.get("dev", "") or scripts.get("start", "")
match = re.search(r'--port\s+(\d+)', dev_script)
if match:
return int(match.group(1))
for script_name in ["dev:h5", "h5", "dev", "start"]:
script = scripts.get(script_name, "")
match = re.search(r'--port(?:=|\s+)(\d+)', script)
if match:
return int(match.group(1))
return None
def _service_has_explicit_port(svc: Service) -> bool:
if _extract_port_from_env(svc.project_dir) is not None:
return True
if svc.project_type in {"vite", "uniapp", "taro", "next"}:
return _extract_port_from_vite_config(svc.project_dir) is not None
return False
def _update_service_port(svc: Service, port: int):
old_port = svc.port
if old_port == port:
return
svc.port = port
svc.health_url = re.sub(r":\d+", f":{port}", svc.health_url, count=1)
updated = []
skip_next = False
for index, part in enumerate(svc.start_cmd):
if skip_next:
skip_next = False
continue
if part == "--port" and index + 1 < len(svc.start_cmd):
updated.extend([part, str(port)])
skip_next = True
elif part.startswith("--port="):
updated.append(f"--port={port}")
elif f"--server.port={old_port}" in part:
updated.append(part.replace(f"--server.port={old_port}", f"--server.port={port}"))
else:
updated.append(part)
svc.start_cmd = updated
def assign_unique_ports(services: list[Service]) -> list[Service]:
reserved = {svc.port for svc in services if _service_has_explicit_port(svc)}
used = set()
for svc in services:
port = svc.port
if port in used or (port in reserved and not _service_has_explicit_port(svc)):
while port in used or port in reserved:
port += 1
log(f"{svc.name} 端口 {svc.port} 与其他服务冲突,自动调整为 {port}", "WARN")
_update_service_port(svc, port)
used.add(svc.port)
return services
def _extract_port_from_pom(pom_path: Path) -> Optional[int]:
"""从 pom.xml 提取 server.port"""
try:
@@ -558,6 +618,38 @@ def _find_python_entry(directory: Path) -> Optional[Path]:
# 服务构建
# ============================================================
def _is_uniapp_project(deps: dict, scripts: dict) -> bool:
dep_names = set(deps.keys())
return (
any(name.startswith("@dcloudio/") for name in dep_names)
or "uni-app" in dep_names
or any("uni " in value or value.startswith("uni") for value in scripts.values())
)
def _is_taro_project(deps: dict, scripts: dict) -> bool:
dep_names = set(deps.keys())
return (
any(name.startswith("@tarojs/") for name in dep_names)
or any("taro " in value or value.startswith("taro") for value in scripts.values())
)
def _get_package_manager(directory: Path) -> str:
if (directory / "pnpm-lock.yaml").exists():
return shutil.which("pnpm") or "pnpm"
if (directory / "yarn.lock").exists():
return shutil.which("yarn") or "yarn"
return shutil.which("npm") or "npm"
def _choose_h5_script(scripts: dict) -> str:
for name in ["dev:h5", "h5", "dev"]:
if name in scripts:
return name
return "dev"
def _get_npm_cmd(directory: Path) -> list:
"""获取可用的包管理器命令,返回 [cmd, args...] 列表"""
# 优先通过 node 直接执行 vite.js(避免 .cmd 的 shell 问题)
@@ -583,6 +675,56 @@ def _get_npm_cmd(directory: Path) -> list:
return ["npx"]
def _build_uniapp_service(directory: Path, pkg_data: dict) -> Service:
name = pkg_data.get("name", "uniapp")
if "/" in name:
name = name.split("/")[-1]
scripts = pkg_data.get("scripts", {})
port = (_extract_port_from_env(directory)
or _extract_port_from_scripts(pkg_data)
or _extract_port_from_vite_config(directory)
or DEFAULT_PORTS["uniapp"])
script = _choose_h5_script(scripts)
cmd = [_get_package_manager(directory), "run", script, "--", "--host", "127.0.0.1", "--port", str(port)]
return Service(
name=f"{name.capitalize()} 前端",
project_type="uniapp",
project_dir=directory,
port=port,
start_cmd=cmd,
health_keyword="",
health_url=f"http://localhost:{port}",
health_timeout=120,
keyword_timeout=0,
)
def _build_taro_service(directory: Path, pkg_data: dict) -> Service:
name = pkg_data.get("name", "taro")
if "/" in name:
name = name.split("/")[-1]
scripts = pkg_data.get("scripts", {})
port = (_extract_port_from_env(directory)
or _extract_port_from_scripts(pkg_data)
or _extract_port_from_vite_config(directory)
or DEFAULT_PORTS["taro"])
script = _choose_h5_script(scripts)
cmd = [_get_package_manager(directory), "run", script, "--", "--host", "127.0.0.1", "--port", str(port)]
return Service(
name=f"{name.capitalize()} 前端",
project_type="taro",
project_dir=directory,
port=port,
start_cmd=cmd,
health_keyword="",
health_url=f"http://localhost:{port}",
health_timeout=120,
keyword_timeout=0,
)
def _build_vite_service(directory: Path, pkg_data: dict) -> Service:
name = pkg_data.get("name", "前端")
if "/" in name:
@@ -1044,15 +1186,20 @@ def check_service_health(svc: Service) -> bool:
log(f"健康检查开始: {svc.name}", "INFO")
log_file = svc.log_path
if wait_for_log_keyword(log_file, svc.health_keyword, svc.keyword_timeout):
if not svc.health_keyword:
log(f"[1/2] Log keyword skipped: {svc.name}", "INFO")
elif wait_for_log_keyword(log_file, svc.health_keyword, svc.keyword_timeout):
log(f"[1/2] 日志就绪: {svc.name}", "SUCCESS")
else:
log(f"[1/2] 日志关键字超时: {svc.name}", "ERROR")
log(f"[1/2] 日志关键字超时: {svc.name}", "WARN")
if http_health_check(svc.health_url, svc.health_timeout):
log(f"[2/2] HTTP ready: {svc.name} ({svc.health_url})", "SUCCESS")
return True
_print_log_tail(svc.name, log_file)
return False
if http_health_check(svc.health_url, svc.health_timeout):
log(f"[2/2] HTTP 就绪: {svc.name} ({svc.health_url})", "SUCCESS")
log(f"[2/2] HTTP ready: {svc.name} ({svc.health_url})", "SUCCESS")
return True
else:
log(f"[2/2] HTTP 健康检查失败: {svc.name}", "ERROR")
@@ -1424,6 +1571,7 @@ def main():
# 发现服务
services = discover_services(root_dir=root_dir)
services = filter_services(services, args.svc_type, args.port)
services = assign_unique_ports(services)
if args.action == "discover":
log_section(f"发现的服务 (目录: {root_dir or CWD})")
+4 -4
View File
@@ -1,9 +1,9 @@
# 开发环境配置(本地开发调试)
VITE_APP_ENV=dev
# 本地环境
# VITE_API_BASE_URL=http://localhost:19089/api
# VITE_WS_URL=ws://localhost:19089/ws
VITE_API_BASE_URL=http://localhost:19089/api
VITE_WS_URL=ws://localhost:19089/ws
# 测试环境
VITE_API_BASE_URL=https://lifescript.happylifeos.com/api
VITE_WS_URL=wss://lifescript.happylifeos.com/ws
# VITE_API_BASE_URL=https://lifescript.happylifeos.com/api
# VITE_WS_URL=wss://lifescript.happylifeos.com/ws
VITE_DEBUG=true
+473 -25
View File
@@ -7,16 +7,21 @@
"name": "emotion-museum-uniapp",
"dependencies": {
"@dcloudio/uni-app": "3.0.0-alpha-5000120260211001",
"@dcloudio/uni-h5": "^3.0.0-alpha-5000120260211001",
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-5000120260211001",
"@vue/runtime-dom": "^3.4.21",
"vue": "3.4.21"
},
"devDependencies": {
"@dcloudio/uni-components": "3.0.0-alpha-5000120260211001",
"@dcloudio/uni-uts-v1": "3.0.0-alpha-5000120260211001",
"@dcloudio/vite-plugin-uni": "3.0.0-alpha-5000120260211001",
"@vitejs/plugin-vue": "^5.2.0",
"@vue/compiler-sfc": "3.4.21",
"@vue/server-renderer": "3.4.21",
"@vue/shared": "3.4.21",
"@vue/tsconfig": "^0.8.1",
"cross-env": "^10.1.0",
"vite": "5.2.8"
}
},
@@ -1917,6 +1922,13 @@
"@dcloudio/types": "3.4.28"
}
},
"node_modules/@dcloudio/uni-app-x": {
"version": "0.7.88",
"resolved": "https://registry.npmmirror.com/@dcloudio/uni-app-x/-/uni-app-x-0.7.88.tgz",
"integrity": "sha512-O+7puNyN8BpovM0RljyvhqpEjon4E53tC/DEBExmObbRBaAcCPtuMm4mdeBK7f9hTQ9KIzxQXohFuX0urVObvw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@dcloudio/uni-cli-shared": {
"version": "3.0.0-alpha-5000120260211001",
"resolved": "https://registry.npmmirror.com/@dcloudio/uni-cli-shared/-/uni-cli-shared-3.0.0-alpha-5000120260211001.tgz",
@@ -2280,6 +2292,168 @@
"debug": "4.3.7"
}
},
"node_modules/@dcloudio/uni-uts-v1": {
"version": "3.0.0-alpha-5000120260211001",
"resolved": "https://registry.npmmirror.com/@dcloudio/uni-uts-v1/-/uni-uts-v1-3.0.0-alpha-5000120260211001.tgz",
"integrity": "sha512-MJ6Kb6eWc6fSfMkeLoPDTeqo3lK4WnUvWfSlSDxLX7VEoPWCv51JktxdgQ+8e7PgX8ZD+Fw5ALFZVxQqxi6OSw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@babel/code-frame": "7.24.7",
"@dcloudio/uni-app-x": "0.7.88",
"@dcloudio/uts": "3.0.0-alpha-5000120260211001",
"@rollup/pluginutils": "5.1.0",
"@vue/shared": "3.4.21",
"adm-zip": "0.5.16",
"android-versions": "^1.8.1",
"colors": "^1.4.0",
"debug": "4.3.7",
"fast-glob": "3.3.3",
"find-cache-dir": "^3.3.2",
"fs-extra": "10.1.0",
"graphlib": "^2.1.8",
"jsonc-parser": "3.3.1",
"lodash": "^4.17.21",
"md5-file": "^5.0.0",
"object-hash": "^3.0.0",
"semver": "7.6.3",
"source-map": "^0.7.4",
"source-map-js": "1.2.1"
}
},
"node_modules/@dcloudio/uni-uts-v1/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@dcloudio/uni-uts-v1/node_modules/source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.7.6.tgz",
"integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">= 12"
}
},
"node_modules/@dcloudio/uts": {
"version": "3.0.0-alpha-5000120260211001",
"resolved": "https://registry.npmmirror.com/@dcloudio/uts/-/uts-3.0.0-alpha-5000120260211001.tgz",
"integrity": "sha512-E9NLFFCtagXRHbG8JeDSkwVmGzvo/4sfekc+z0SOXhiDUr95WMwtyNW11FptOh+MzpmXwuPVg2AH+OB1jkB5+Q==",
"dev": true,
"optionalDependencies": {
"@dcloudio/uts-darwin-arm64": "3.0.0-alpha-5000120260211001",
"@dcloudio/uts-darwin-x64": "3.0.0-alpha-5000120260211001",
"@dcloudio/uts-linux-x64-gnu": "3.0.0-alpha-5000120260211001",
"@dcloudio/uts-linux-x64-musl": "3.0.0-alpha-5000120260211001",
"@dcloudio/uts-win32-ia32-msvc": "3.0.0-alpha-5000120260211001",
"@dcloudio/uts-win32-x64-msvc": "3.0.0-alpha-5000120260211001"
}
},
"node_modules/@dcloudio/uts-darwin-arm64": {
"version": "3.0.0-alpha-5000120260211001",
"resolved": "https://registry.npmmirror.com/@dcloudio/uts-darwin-arm64/-/uts-darwin-arm64-3.0.0-alpha-5000120260211001.tgz",
"integrity": "sha512-UPtg2Z1OqxQyzB/uAolBoe8jubMxUXURqjfoeFN+ChpRcQDKUnqwp/at2N8peIQL4s2y1GwnCjV3FJ7n76Ca2Q==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@dcloudio/uts-darwin-x64": {
"version": "3.0.0-alpha-5000120260211001",
"resolved": "https://registry.npmmirror.com/@dcloudio/uts-darwin-x64/-/uts-darwin-x64-3.0.0-alpha-5000120260211001.tgz",
"integrity": "sha512-8URCkpWw4q7x6uebjeRq2FmxwzckDTf2EM/ztV2A+MLwCn46b1yRgIJJMSVQioATvhdOA5v78pZVuEG/Old1zQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@dcloudio/uts-linux-x64-gnu": {
"version": "3.0.0-alpha-5000120260211001",
"resolved": "https://registry.npmmirror.com/@dcloudio/uts-linux-x64-gnu/-/uts-linux-x64-gnu-3.0.0-alpha-5000120260211001.tgz",
"integrity": "sha512-mlfvpQQcTOBf/amWIRxDGKQh3GItBI7jflY7bsePU5iMJi/gHOnv22wRBvcuNwIdNydbjskwDNgnESLwZAF6pg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@dcloudio/uts-linux-x64-musl": {
"version": "3.0.0-alpha-5000120260211001",
"resolved": "https://registry.npmmirror.com/@dcloudio/uts-linux-x64-musl/-/uts-linux-x64-musl-3.0.0-alpha-5000120260211001.tgz",
"integrity": "sha512-+RTLm8dJQsqFV47ImsN1+90gNFUgTioOVYp5HA/EWXZzmCNrXadx+JGUurtON4j8Hzm3/TnWp4o2lv+uCCFFnA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@dcloudio/uts-win32-ia32-msvc": {
"version": "3.0.0-alpha-5000120260211001",
"resolved": "https://registry.npmmirror.com/@dcloudio/uts-win32-ia32-msvc/-/uts-win32-ia32-msvc-3.0.0-alpha-5000120260211001.tgz",
"integrity": "sha512-wpNdG9uby8XUd/pkv7U3uanD8e0T/+dqwwGMdY+VGfIajPe4c8Tl9ouACmvwQrCKumrVIgCKIj247MCCytOiTg==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@dcloudio/uts-win32-x64-msvc": {
"version": "3.0.0-alpha-5000120260211001",
"resolved": "https://registry.npmmirror.com/@dcloudio/uts-win32-x64-msvc/-/uts-win32-x64-msvc-3.0.0-alpha-5000120260211001.tgz",
"integrity": "sha512-GbwAEwuhJrcWN/8S05/II80cXx/bPH+okU9aaJpN26G1PcDxTWu9mxbYRzsEl89QxhBwQE2e7P4TRripmeEvFQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@dcloudio/vite-plugin-uni": {
"version": "3.0.0-alpha-5000120260211001",
"resolved": "https://registry.npmmirror.com/@dcloudio/vite-plugin-uni/-/vite-plugin-uni-3.0.0-alpha-5000120260211001.tgz",
@@ -2380,6 +2554,13 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/@epic-web/invariant": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/@epic-web/invariant/-/invariant-1.0.0.tgz",
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
"dev": true,
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@@ -3445,7 +3626,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3459,7 +3639,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3473,7 +3652,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3487,7 +3665,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3501,7 +3678,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3515,7 +3691,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3529,7 +3704,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3543,7 +3717,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3557,7 +3730,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3571,7 +3743,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3585,7 +3756,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3599,7 +3769,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3613,7 +3782,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3627,7 +3795,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3641,7 +3808,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3655,7 +3821,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3669,7 +3834,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3683,7 +3847,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3697,7 +3860,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3711,7 +3873,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3725,7 +3886,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3739,7 +3899,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3753,7 +3912,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3767,7 +3925,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3781,7 +3938,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4222,6 +4378,29 @@
"node": ">=12.0"
}
},
"node_modules/android-versions": {
"version": "1.9.0",
"resolved": "https://registry.npmmirror.com/android-versions/-/android-versions-1.9.0.tgz",
"integrity": "sha512-13O2B6PQMEM4ej9n13ePRQeckrCoKbZrvuzlLvK+9s2QmncpHDbYzZxhgapN32sJNoifN6VAHexLnd/6CYrs7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.2"
}
},
"node_modules/android-versions/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz",
@@ -4693,6 +4872,16 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"license": "MIT"
},
"node_modules/colors": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz",
@@ -4700,6 +4889,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true,
"license": "MIT"
},
"node_modules/compare-versions": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/compare-versions/-/compare-versions-3.6.0.tgz",
@@ -4794,6 +4990,39 @@
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-env": {
"version": "10.1.0",
"resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-10.1.0.tgz",
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@epic-web/invariant": "^1.0.0",
"cross-spawn": "^7.0.6"
},
"bin": {
"cross-env": "dist/bin/cross-env.js",
"cross-env-shell": "dist/bin/cross-env-shell.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/css-font-size-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz",
@@ -5232,6 +5461,38 @@
"dev": true,
"license": "MIT"
},
"node_modules/find-cache-dir": {
"version": "3.3.2",
"resolved": "https://registry.npmmirror.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
"integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
"dev": true,
"license": "MIT",
"dependencies": {
"commondir": "^1.0.1",
"make-dir": "^3.0.2",
"pkg-dir": "^4.1.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/avajs/find-cache-dir?sponsor=1"
}
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
@@ -5420,6 +5681,16 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/graphlib": {
"version": "2.1.8",
"resolved": "https://registry.npmmirror.com/graphlib/-/graphlib-2.1.8.tgz",
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash": "^4.17.15"
}
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz",
@@ -5632,6 +5903,13 @@
"url": "https://github.com/sponsors/gjtorikian/"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/jimp": {
"version": "0.10.3",
"resolved": "https://registry.npmmirror.com/jimp/-/jimp-0.10.3.tgz",
@@ -5835,6 +6113,26 @@
"node": ">=6"
}
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@@ -5866,6 +6164,22 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5876,6 +6190,19 @@
"node": ">= 0.4"
}
},
"node_modules/md5-file": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/md5-file/-/md5-file-5.0.0.tgz",
"integrity": "sha512-xbEFXCYVWrSx/gEKS1VPlg84h/4L20znVIulKw6kMfmBUAZNAnF00eczz9ICMl+/hjQGo5KSXRxbL/47X3rmMw==",
"dev": true,
"license": "MIT",
"bin": {
"md5-file": "cli.js"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
@@ -6088,6 +6415,16 @@
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -6133,6 +6470,45 @@
"yarn": "^1.22.4"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
@@ -6192,6 +6568,26 @@
"node": ">= 0.8"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
@@ -6257,6 +6653,19 @@
"pixelmatch": "bin/pixelmatch"
}
},
"node_modules/pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz",
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"find-up": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz",
@@ -6987,6 +7396,29 @@
"dev": true,
"license": "ISC"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
@@ -7612,6 +8044,22 @@
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.0.tgz",
+2
View File
@@ -11,7 +11,9 @@
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-alpha-5000120260211001",
"@dcloudio/uni-h5": "^3.0.0-alpha-5000120260211001",
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-5000120260211001",
"@vue/runtime-dom": "^3.4.21",
"vue": "3.4.21"
},
"devDependencies": {
+61 -365
View File
@@ -2,6 +2,7 @@
import { ref } from 'vue'
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import { useAppStore } from './stores/app.js'
import { logRuntimeEnv } from './services/request.js'
const statusBarHeight = ref(0)
const safeAreaTop = ref(0)
@@ -9,14 +10,11 @@ const safeAreaBottom = ref(0)
onLaunch(async () => {
console.log('App Launch')
logRuntimeEnv('app:onLaunch')
const store = useAppStore()
await store.initialize()
// 使用新的推荐 API 替代已弃用的 getSystemInfoSync
const deviceInfo = uni.getDeviceInfo()
const windowInfo = uni.getWindowInfo()
const appBaseInfo = uni.getAppBaseInfo()
statusBarHeight.value = windowInfo.statusBarHeight || 20
safeAreaTop.value = windowInfo.safeAreaInsets?.top || windowInfo.statusBarHeight || 20
safeAreaBottom.value = windowInfo.safeAreaInsets?.bottom || 0
@@ -35,318 +33,36 @@ onHide(() => {
</script>
<style>
page {
background-color: #0F071A;
color: #F3E8FF;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
font-size: 28rpx;
line-height: 1.5;
width: 100%;
height: 100%;
overflow: hidden;
-webkit-overflow-scrolling: touch;
color: #f8f4ff;
background: #050615;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 28rpx;
line-height: 1.45;
-webkit-font-smoothing: antialiased;
}
/* 标题字体 - Cinzel (原型标准) */
.font-serif, .page-title, .section-title {
font-family: 'Cinzel', 'Inter', serif;
}
/* 星空背景 */
.stars-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
}
.star {
position: absolute;
background: white;
border-radius: 50%;
opacity: var(--opacity);
animation: float-star var(--duration) ease-in-out infinite;
animation-delay: var(--delay);
}
@keyframes float-star {
0%, 100% {
transform: translate(0, 0) scale(1);
opacity: var(--opacity);
}
50% {
transform: translate(var(--x), var(--y)) scale(1.5);
opacity: 1;
}
}
.status-bar-space {
width: 100%;
button {
padding: 0;
margin: 0;
border: 0;
color: inherit;
background: transparent;
flex-shrink: 0;
line-height: normal;
font: inherit;
}
.safe-area-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
button::after {
border: 0;
}
.safe-area-top {
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
}
/* ==================== 玻璃态卡片 - 原型标准 ==================== */
.glass-card {
background: rgba(168, 85, 247, 0.05);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(168, 85, 247, 0.15);
border-radius: 40rpx; /* 原型标准:2.5rem = 40px */
transition: all 0.3s ease;
}
.glass-card:active {
transform: scale(0.98);
box-shadow: 0 4rpx 20rpx rgba(168, 85, 247, 0.05);
}
.glass-card-gold {
background: linear-gradient(135deg, rgba(168, 85, 247, 0.15), rgba(232, 121, 249, 0.1));
border: 1px solid rgba(168, 85, 247, 0.3);
border-radius: 40rpx;
box-shadow: inset 0 0 20rpx rgba(168, 85, 247, 0.08),
0 0 20px rgba(168, 85, 247, 0.1);
transition: all 0.3s ease;
}
.glass-card-gold:active {
transform: scale(0.98);
box-shadow: inset 0 0 10rpx rgba(168, 85, 247, 0.05),
0 2rpx 12px rgba(168, 85, 247, 0.08);
}
/* 原型标准输入框 - 40rpx 圆角 */
.glass-input {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(168, 85, 247, 0.15);
border-radius: 40rpx; /* 原型标准:2.5rem = 40px */
padding: 24rpx 32rpx;
color: #F3E8FF;
font-size: 28rpx;
transition: all 0.3s ease;
}
.glass-input:active {
background: rgba(168, 85, 247, 0.1);
border-color: rgba(168, 85, 247, 0.4);
box-shadow: 0 0 15px rgba(168, 85, 247, 0.3);
}
.glass-input::placeholder {
color: rgba(255, 255, 255, 0.3);
}
/* 发光边框 - 头像/卡片专用 */
.glow-border {
box-shadow: 0 0 20px rgba(168, 85, 247, 0.4);
}
/* 强发光效果 - 头像专用 */
.glow-strong {
box-shadow: 0 0 40px rgba(168, 85, 247, 0.1);
}
/* 左侧紫色边框 - 记录卡片专用 */
.border-left-purple {
border-left: 4rpx solid #C084FC;
}
.btn-primary {
background: linear-gradient(135deg, #9333EA 0%, #7C3AED 100%);
border-radius: 32rpx;
padding: 28rpx 48rpx;
color: white !important;
font-weight: 600;
font-size: 30rpx;
border: none;
box-shadow: 0 8rpx 32rpx rgba(168, 85, 247, 0.3);
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.btn-primary:active {
transform: scale(0.98);
opacity: 0.9;
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 24rpx;
padding: 24rpx 40rpx;
color: rgba(255, 255, 255, 0.8);
font-size: 28rpx;
}
.bottom-nav {
background: rgba(15, 7, 26, 0.85);
backdrop-filter: blur(40rpx);
-webkit-backdrop-filter: blur(40rpx);
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.nav-item {
color: rgba(255, 255, 255, 0.3);
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
}
.nav-item.active {
color: #C084FC;
transform: translateY(-8rpx);
text-shadow: 0 0 15px rgba(168, 85, 247, 0.6);
}
.step-indicator {
display: flex;
gap: 12rpx;
}
.step-dot {
width: 24rpx;
height: 8rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 4rpx;
transition: all 0.7s ease;
}
.step-dot.active {
width: 40rpx;
background: #A855F7;
box-shadow: 0 0 24rpx rgba(168, 85, 247, 0.6);
}
.hint-chip {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 32rpx;
padding: 12rpx 24rpx;
font-size: 22rpx;
color: rgba(243, 232, 255, 0.8);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
backdrop-filter: blur(8rpx);
transition: all 0.2s ease;
}
.hint-chip:active {
background: rgba(168, 85, 247, 0.2);
border-color: rgba(168, 85, 247, 0.4);
transform: scale(1.05);
box-shadow: 0 8rpx 15rpx rgba(168, 85, 247, 0.3);
}
.ai-reply-card {
background: rgba(168, 85, 247, 0.08);
border: 1px solid rgba(168, 85, 247, 0.2);
border-radius: 20rpx;
padding: 24rpx;
box-shadow: 0 0 20px rgba(168, 85, 247, 0.1);
}
.typing-text {
overflow: hidden;
white-space: pre-wrap;
animation: reveal 2s steps(60, end);
}
@keyframes reveal {
from { opacity: 0; transform: translateY(10rpx); }
to { opacity: 1; transform: translateY(0); }
}
/* 星芒加载动画 - 原型标准(双环旋转) */
.starlight-loader {
position: relative;
width: 80rpx;
height: 80rpx;
margin: 40rpx auto;
}
.starlight-loader::before,
.starlight-loader::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
border-radius: 50%;
border: 3rpx solid transparent;
border-top-color: #C084FC;
border-right-color: #A855F7;
animation: starlight-spin 1.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.starlight-loader::before {
width: 70%;
height: 70%;
border-top-color: #E879F9;
border-right-color: #9333EA;
animation-delay: -0.75s;
}
@keyframes starlight-spin {
0% {
transform: translate(-50%, -50%) rotate(0deg) scale(1);
}
50% {
transform: translate(-50%, -50%) rotate(180deg) scale(1.2);
}
100% {
transform: translate(-50%, -50%) rotate(360deg) scale(1);
}
}
/* 加载文字 - 脉冲效果 */
.loading-text {
text-align: center;
font-size: 24rpx;
color: rgba(192, 132, 252, 0.6);
letter-spacing: 4rpx;
margin-top: 24rpx;
animation: pulse-text 2s ease-in-out infinite;
}
@keyframes pulse-text {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
/* 简化 loader 备用 */
.starlight-loader-simple {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
border: 2rpx solid #A855F7;
border-top-color: transparent;
animation: spin 0.8s infinite linear;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
input,
textarea {
color: #f8f4ff;
font: inherit;
}
::-webkit-scrollbar {
@@ -355,65 +71,6 @@ page {
display: none;
}
::-webkit-scrollbar-thumb {
background: transparent;
}
::-webkit-scrollbar-track {
background: transparent;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20rpx); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.5s ease-out;
}
@keyframes pulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 0.7; }
}
.animate-pulse-slow {
animation: pulse 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10rpx); }
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
.text-primary { color: #A855F7; }
.text-primary-light { color: #C084FC; }
.text-accent { color: #E879F9; }
.text-muted { color: rgba(255, 255, 255, 0.4); }
.text-white-90 { color: rgba(255, 255, 255, 0.9); }
.text-white-70 { color: rgba(255, 255, 255, 0.7); }
.text-white-50 { color: rgba(255, 255, 255, 0.5); }
.font-serif { font-family: 'Cinzel', serif; }
.bg-dark { background-color: #0F071A; }
.bg-gradient-purple {
background: linear-gradient(180deg, #1A0B2E 0%, #0F071A 50%, #050208 100%);
}
.bg-aurora-top {
background: rgba(168, 85, 247, 0.08);
filter: blur(120rpx);
border-radius: 50%;
}
.bg-aurora-bottom {
background: rgba(139, 92, 246, 0.05);
filter: blur(100rpx);
border-radius: 50%;
}
.safe-area-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
@@ -423,4 +80,43 @@ page {
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
}
.kos-page {
min-height: 100vh;
color: #f8f4ff;
background:
radial-gradient(circle at 78% 10%, rgba(111, 55, 255, 0.32), transparent 28%),
radial-gradient(circle at 14% 34%, rgba(36, 124, 255, 0.17), transparent 28%),
linear-gradient(180deg, #07091b 0%, #07031a 46%, #04030f 100%);
}
.kos-card {
border: 1rpx solid rgba(163, 92, 255, 0.38);
background:
linear-gradient(145deg, rgba(31, 23, 76, 0.78), rgba(8, 8, 30, 0.9)),
radial-gradient(circle at 90% 20%, rgba(139, 78, 255, 0.14), transparent 38%);
box-shadow:
inset 0 0 44rpx rgba(147, 92, 255, 0.08),
0 22rpx 64rpx rgba(0, 0, 0, 0.26);
backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx);
}
.kos-pill {
border: 1rpx solid rgba(157, 108, 255, 0.34);
background: rgba(255, 255, 255, 0.055);
box-shadow: inset 0 0 20rpx rgba(160, 96, 255, 0.08);
}
.kos-primary {
color: #fff;
background:
radial-gradient(circle at 68% 20%, rgba(255, 255, 255, 0.28), transparent 22%),
linear-gradient(135deg, #b045ff 0%, #612eff 56%, #2b1aff 100%);
box-shadow: 0 18rpx 42rpx rgba(129, 66, 255, 0.38), inset 0 0 20rpx rgba(255, 255, 255, 0.1);
}
.placeholder {
color: rgba(213, 199, 239, 0.42);
}
</style>
+28 -3
View File
@@ -6,7 +6,7 @@
@click="toggleMusic"
>
<view class="music-disc" :class="{ spinning: isPlaying }"></view>
<text class="music-icon">🎵</text>
<view class="music-note"></view>
</view>
</view>
</template>
@@ -127,8 +127,33 @@ defineExpose({
to { transform: rotate(360deg); }
}
.music-icon {
font-size: 36rpx;
.music-note {
position: relative;
width: 26rpx;
height: 38rpx;
z-index: 1;
}
.music-note::before {
content: '';
position: absolute;
left: 2rpx;
bottom: 0;
width: 18rpx;
height: 14rpx;
border: 5rpx solid rgba(196, 181, 253, 0.86);
border-radius: 50%;
}
.music-note::after {
content: '';
position: absolute;
right: 0;
top: 0;
width: 9rpx;
height: 34rpx;
border-radius: 6rpx;
background: rgba(196, 181, 253, 0.86);
box-shadow: 8rpx 3rpx 0 rgba(196, 181, 253, 0.42);
}
</style>
+22
View File
@@ -31,9 +31,31 @@
"navigationBarTitleText": "剧本详情"
}
},
{
"path": "pages/main/PathView",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "实现路径"
}
},
{
"path": "pages/life-event/form",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "记录人生经历"
}
},
{
"path": "pages/life-event/detail",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "人生事件"
}
},
{
"path": "pages/profile/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "个人中心"
}
}
@@ -0,0 +1,310 @@
<template>
<view class="event-detail-page">
<view class="space-bg"></view>
<view class="top-safe" :style="{ height: safeAreaTop + 'px' }"></view>
<view class="header">
<text class="back" @click="goBack"></text>
<text class="title">人生轨迹内容</text>
<text class="more"></text>
</view>
<scroll-view class="content" scroll-y :show-scrollbar="false">
<view class="hero-card kos-card">
<view class="cover">{{ (event.title || '轨').slice(0, 1) }}</view>
<view class="hero-body">
<text class="date">{{ event.time || event.date || '未知日期' }}</text>
<text class="event-title">{{ event.title || '未命名经历' }}</text>
<text class="tag">{{ primaryTag }}</text>
</view>
</view>
<view class="panel kos-card">
<text class="panel-title">经历描述</text>
<text class="body-text">{{ event.content || event.description || '暂无内容。' }}</text>
</view>
<view class="panel ai-panel kos-card">
<view class="ai-head">
<text class="spark"></text>
<text>AI 人生解读</text>
</view>
<view v-for="block in analysisBlocks" :key="block.title" class="analysis-block">
<text class="analysis-title">{{ block.title }}</text>
<text class="analysis-text">{{ block.text }}</text>
</view>
</view>
<view class="related-tags">
<text v-for="tag in relatedTags" :key="tag" class="tag-pill kos-pill">{{ tag }}</text>
</view>
</scroll-view>
<view class="bottom-actions" :style="{ paddingBottom: safeAreaBottom + 14 + 'px' }">
<view class="action kos-pill" @click="notReady('编辑')">编辑</view>
<view class="action kos-pill" @click="notReady('收藏')">收藏</view>
<view class="action primary kos-primary" @click="notReady('聊天')">聊聊这段经历</view>
<view class="action kos-pill" @click="notReady('分享')">分享</view>
</view>
</view>
</template>
<script setup>
import { computed, ref, onMounted } from 'vue'
import { useAppStore } from '../../stores/app.js'
const store = useAppStore()
const safeAreaTop = ref(20)
const safeAreaBottom = ref(0)
const eventId = ref('')
onMounted(async () => {
const info = uni.getWindowInfo()
safeAreaTop.value = info.safeAreaInsets?.top || info.statusBarHeight || 20
safeAreaBottom.value = info.safeAreaInsets?.bottom || 0
const pages = getCurrentPages()
eventId.value = pages[pages.length - 1]?.options?.id || ''
if (!store.events?.length) await store.fetchEvents()
})
const event = computed(() => store.getEventById(eventId.value) || {})
const primaryTag = computed(() => Array.isArray(event.value.tags) && event.value.tags.length ? event.value.tags[0] : '人生片段')
const relatedTags = computed(() => {
if (Array.isArray(event.value.tags) && event.value.tags.length) return event.value.tags
return ['理解', '成长', '转折']
})
const analysisBlocks = computed(() => {
const ai = event.value.aiFeedback
if (!ai) {
return [
{ title: '情绪整理', text: '这段经历还没有 AI 解读,但它已经是一枚重要的生命坐标。' },
{ title: '成长意义', text: '你可以继续补充当时的选择、关系和感受,让它成为后续剧本生成的素材。' }
]
}
return [
{ title: '情绪整理', text: ai.slice(0, 90) },
{ title: '能力映射', text: '从这段经历里,可以看到你对变化的感知、承受和重新组织能力。' },
{ title: '下一步建议', text: '把这段经历与一个目标连接起来,它会更容易转化成行动路径。' }
]
})
const notReady = (label) => {
uni.showToast({ title: `${label}能力稍后开放`, icon: 'none' })
}
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
.event-detail-page {
position: relative;
height: 100vh;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
color: #fff;
background: #050615;
}
.space-bg {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 18% 2%, rgba(124, 58, 237, 0.3), transparent 34%),
radial-gradient(circle at 88% 22%, rgba(14, 165, 233, 0.18), transparent 28%),
linear-gradient(180deg, #07091d 0%, #07031a 52%, #04020e 100%);
}
.top-safe,
.header,
.content,
.bottom-actions {
position: relative;
z-index: 1;
}
.header {
height: 92rpx;
flex-shrink: 0;
display: grid;
grid-template-columns: 80rpx 1fr 80rpx;
align-items: center;
padding: 0 28rpx;
}
.back {
font-size: 66rpx;
color: #fff;
}
.title {
text-align: center;
font-size: 34rpx;
font-weight: 900;
}
.more {
text-align: right;
color: rgba(255, 255, 255, 0.68);
letter-spacing: 4rpx;
}
.content {
flex: 1;
height: 0;
min-height: 0;
box-sizing: border-box;
padding: 0 30rpx;
}
.hero-card,
.panel {
border-radius: 30rpx;
margin-bottom: 24rpx;
}
.hero-card {
overflow: hidden;
}
.cover {
height: 256rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 88rpx;
font-weight: 900;
background:
radial-gradient(circle at 30% 20%, rgba(56, 189, 248, 0.3), transparent 30%),
linear-gradient(135deg, #7d35ff, #111827);
}
.hero-body,
.panel {
padding: 30rpx;
}
.date,
.tag,
.body-text,
.analysis-text {
display: block;
color: rgba(226, 216, 246, 0.72);
}
.date {
font-size: 22rpx;
letter-spacing: 3rpx;
}
.event-title {
display: block;
margin-top: 12rpx;
color: #fff;
font-size: 40rpx;
font-weight: 900;
line-height: 1.24;
}
.tag {
width: fit-content;
margin-top: 18rpx;
padding: 9rpx 18rpx;
border-radius: 999rpx;
color: #caa0ff;
background: rgba(124, 58, 237, 0.18);
}
.panel-title {
display: block;
margin-bottom: 16rpx;
color: #fff;
font-size: 30rpx;
font-weight: 900;
}
.body-text {
font-size: 26rpx;
line-height: 1.75;
}
.ai-head {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 18rpx;
color: #fff;
font-size: 30rpx;
font-weight: 900;
}
.spark {
color: #d982ff;
}
.analysis-block {
padding: 20rpx 0;
border-top: 1rpx solid rgba(180, 139, 255, 0.14);
}
.analysis-title {
display: block;
margin-bottom: 10rpx;
color: #dccbff;
font-size: 24rpx;
font-weight: 900;
}
.analysis-text {
font-size: 24rpx;
line-height: 1.65;
}
.related-tags {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
padding-bottom: 34rpx;
}
.tag-pill {
height: 48rpx;
padding: 0 18rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
color: rgba(237, 233, 254, 0.78);
font-size: 22rpx;
}
.bottom-actions {
min-height: 124rpx;
box-sizing: border-box;
display: grid;
grid-template-columns: 0.8fr 0.8fr 1.5fr 0.8fr;
gap: 12rpx;
padding: 14rpx 22rpx;
background: rgba(5, 6, 21, 0.72);
backdrop-filter: blur(24rpx);
}
.action {
height: 72rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: #caa0ff;
font-size: 23rpx;
font-weight: 700;
}
.action.primary {
color: #fff;
}
</style>
+404
View File
@@ -0,0 +1,404 @@
<template>
<view class="event-form-page">
<view class="space-bg"></view>
<view class="top-safe" :style="{ height: safeAreaTop + 'px' }"></view>
<view class="header">
<text class="back" @click="goBack"></text>
<view>
<text class="title">记录人生经历 </text>
<text class="subtitle">记录每一个重要时刻AI将帮你生成专属人生轨迹</text>
</view>
<text class="save" @click="submit">保存</text>
</view>
<scroll-view class="content" scroll-y :show-scrollbar="false">
<view class="form-card kos-card">
<view class="group">
<view class="group-title">
<text class="group-icon"></text>
<text>时间</text>
</view>
<view class="segmented">
<text v-for="item in timeModes" :key="item" class="seg active-first">{{ item }}</text>
</view>
<picker mode="date" :value="form.time" @change="e => form.time = e.detail.value">
<view class="date-picker field-box">
<text>{{ formatDate(form.time) }}</text>
<text class="chevron"></text>
</view>
</picker>
</view>
<view class="group">
<view class="label-row">
<view class="group-title">
<text class="group-icon"></text>
<text>事件标题 </text>
</view>
<text class="count">{{ form.title.length }}/30</text>
</view>
<input class="field-box input" maxlength="30" v-model="form.title" placeholder="给这段经历起个标题吧..." placeholder-class="placeholder" />
</view>
<view class="group">
<view class="label-row">
<view class="group-title">
<text class="group-icon"></text>
<text>具体内容 </text>
</view>
<text class="count">{{ form.content.length }}/500</text>
</view>
<view class="textarea-wrap">
<textarea class="textarea" maxlength="500" v-model="form.content" placeholder="请详细记录这段经历的背景、发生的事情、你的感受和收获..." placeholder-class="placeholder" />
<view class="ai-btn kos-pill" @click="assistWrite"> AI 帮我写</view>
</view>
</view>
<view class="group">
<view class="label-row">
<view class="group-title">
<text class="group-icon"></text>
<text>相关标签</text>
</view>
<view class="custom-tag kos-pill"> 自定义标签</view>
</view>
<view class="tag-grid">
<text
v-for="tag in tags"
:key="tag"
class="tag kos-pill"
:class="{ active: form.tags.includes(tag) }"
@click="toggleTag(tag)"
>{{ tag }}</text>
</view>
</view>
<button class="submit kos-primary" :loading="saving" @click="submit">
<text> 提交记录</text>
<text class="submit-sub">记录后可在时间轴查看</text>
</button>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { useAppStore } from '../../stores/app.js'
const store = useAppStore()
const safeAreaTop = ref(20)
const saving = ref(false)
const form = reactive({
title: '',
time: new Date().toISOString().slice(0, 10),
content: '',
tags: [],
eventType: 'daily_log'
})
const timeModes = ['具体日期', '年月', '季节', '时间轴范围']
const tags = ['成长', '学习', '工作', '旅行', '感情', '家庭', '友情', '挑战', '突破', '收获', '感动', '迷茫']
onMounted(() => {
const info = uni.getWindowInfo()
safeAreaTop.value = info.safeAreaInsets?.top || info.statusBarHeight || 20
})
const formatDate = (value) => {
if (!value) return '选择具体日期'
const [year, month, day] = value.split('-')
return `${year}${month}${day}`
}
const toggleTag = (tag) => {
const index = form.tags.indexOf(tag)
if (index >= 0) form.tags.splice(index, 1)
else form.tags.push(tag)
}
const assistWrite = () => {
if (!form.content) {
form.content = '那一天,我清楚地感受到自己正在经历一次变化。事情本身也许并不宏大,但它让我重新看见了自己的选择、情绪和力量。'
}
}
const submit = async () => {
if (!form.title || !form.content || saving.value) {
uni.showToast({ title: '请填写标题和内容', icon: 'none' })
return
}
saving.value = true
const result = await store.createEvent({ ...form, aiFeedback: buildAiFeedback() })
saving.value = false
if (result.success) {
uni.navigateBack()
} else {
uni.showToast({ title: result.error || '保存失败', icon: 'none' })
}
}
const buildAiFeedback = () => {
return '这段经历体现了你的自我观察能力。它可能意味着你正在从旧的反应模式里走出来,开始更主动地理解自己的处境。建议保留当时的细节,因为这些细节会成为后续人生剧本的重要素材。'
}
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
.event-form-page {
position: relative;
height: 100vh;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
color: #fff;
background: #050615;
}
.space-bg {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 86% 88%, rgba(126, 57, 255, 0.34), transparent 30%),
radial-gradient(circle at 18% 8%, rgba(63, 120, 255, 0.18), transparent 26%),
linear-gradient(180deg, #07091d 0%, #07031a 52%, #04020e 100%);
}
.top-safe,
.header,
.content {
position: relative;
z-index: 1;
}
.top-safe,
.header {
flex-shrink: 0;
}
.header {
min-height: 138rpx;
display: grid;
grid-template-columns: 72rpx 1fr 72rpx;
align-items: center;
padding: 0 28rpx;
}
.back {
color: #fff;
font-size: 70rpx;
}
.title,
.subtitle {
display: block;
text-align: center;
}
.title {
color: #fff;
font-size: 36rpx;
font-weight: 900;
}
.subtitle {
margin-top: 14rpx;
color: rgba(218, 204, 243, 0.72);
font-size: 24rpx;
}
.save {
text-align: right;
color: #c06dff;
font-size: 27rpx;
}
.content {
flex: 1;
height: 0;
min-height: 0;
box-sizing: border-box;
padding: 0 28rpx 34rpx;
}
.form-card {
border-radius: 34rpx;
padding: 30rpx;
}
.group + .group {
margin-top: 34rpx;
}
.group-title,
.label-row {
display: flex;
align-items: center;
}
.label-row {
justify-content: space-between;
}
.group-title {
gap: 14rpx;
color: #d4b7ff;
font-size: 31rpx;
font-weight: 900;
}
.group-icon {
color: #a855ff;
font-size: 38rpx;
}
.count,
.custom-tag {
color: #b58bff;
font-size: 23rpx;
}
.custom-tag {
height: 50rpx;
padding: 0 18rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
}
.segmented {
height: 66rpx;
margin-top: 24rpx;
border-radius: 999rpx;
padding: 5rpx;
display: grid;
grid-template-columns: repeat(4, 1fr);
border: 1rpx solid rgba(151, 111, 255, 0.26);
background: rgba(11, 12, 38, 0.7);
}
.seg {
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(224, 214, 243, 0.72);
font-size: 23rpx;
}
.seg:first-child {
color: #fff;
background: linear-gradient(135deg, #a843ff, #7630ff);
box-shadow: 0 0 24rpx rgba(168, 67, 255, 0.5);
}
.field-box,
.textarea {
width: 100%;
box-sizing: border-box;
border-radius: 22rpx;
border: 1rpx solid rgba(151, 111, 255, 0.24);
background: rgba(12, 15, 46, 0.72);
color: #fff;
font-size: 27rpx;
}
.date-picker {
height: 78rpx;
margin-top: 22rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.chevron {
color: rgba(224, 214, 243, 0.62);
font-size: 52rpx;
}
.input {
height: 82rpx;
margin-top: 18rpx;
padding: 0 24rpx;
}
.textarea-wrap {
position: relative;
margin-top: 18rpx;
}
.textarea {
height: 258rpx;
padding: 24rpx;
line-height: 1.6;
}
.ai-btn {
position: absolute;
right: 18rpx;
bottom: 18rpx;
height: 54rpx;
padding: 0 18rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
color: #d5b3ff;
font-size: 23rpx;
}
.tag-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16rpx;
margin-top: 22rpx;
}
.tag {
height: 58rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(224, 214, 243, 0.68);
font-size: 24rpx;
}
.tag.active {
color: #fff;
border-color: rgba(192, 100, 255, 0.84);
background: rgba(137, 51, 255, 0.35);
box-shadow: 0 0 22rpx rgba(168, 67, 255, 0.34);
}
.submit {
width: 100%;
height: 112rpx;
margin-top: 38rpx;
border-radius: 28rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.submit text {
display: block;
color: #fff;
font-size: 31rpx;
font-weight: 900;
}
.submit-sub {
margin-top: 8rpx;
color: rgba(255, 255, 255, 0.72) !important;
font-size: 23rpx !important;
font-weight: 500 !important;
}
</style>
+1 -1
View File
@@ -164,7 +164,7 @@ const handleLogin = async () => {
radial-gradient(circle at 50% -8%, rgba(180, 129, 255, 0.28), transparent 28%),
linear-gradient(180deg, #13091f 0%, #1b0b31 46%, #100719 100%);
position: relative;
overflow: hidden;
overflow: visible;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
+248
View File
@@ -0,0 +1,248 @@
<template>
<view class="mine-view">
<view class="profile-card kos-card">
<view class="avatar">{{ avatarText }}</view>
<view class="profile-main">
<view class="name-row">
<text class="name">{{ profile.nickname || 'Zoey' }}</text>
<text class="star"></text>
</view>
<text class="signature">{{ signature }}</text>
<view class="chips">
<text v-for="chip in chips" :key="chip" class="chip kos-pill">{{ chip }}</text>
</view>
</view>
<view class="edit-btn kos-pill" @click="editProfile">编辑资料</view>
</view>
<view class="stats-grid">
<view class="stat-card kos-card">
<text class="stat-value">{{ eventsCount }}</text>
<text class="stat-label">人生记录</text>
</view>
<view class="stat-card kos-card">
<text class="stat-value">{{ scriptsCount }}</text>
<text class="stat-label">生成剧本</text>
</view>
</view>
<view class="section-card kos-card">
<view class="section-title">兴趣爱好</view>
<view class="tag-cloud">
<text v-for="tag in hobbyTags" :key="tag" class="tag kos-pill">{{ tag }}</text>
</view>
</view>
<view class="section-card kos-card">
<view class="section-title">生命摘要</view>
<view class="memory-line">
<text class="memory-label">童年</text>
<text class="memory-text">{{ profile.childhood?.text || '还没有写下最早的光。' }}</text>
</view>
<view class="memory-line">
<text class="memory-label">高光</text>
<text class="memory-text">{{ profile.joy?.text || '等待记录一次会发光的瞬间。' }}</text>
</view>
<view class="memory-line">
<text class="memory-label">未来</text>
<text class="memory-text">{{ profile.future?.vision || '未来档案还在生成中。' }}</text>
</view>
</view>
<button class="logout kos-pill" @click="handleLogout">退出登录</button>
</view>
</template>
<script setup>
import { computed } from 'vue'
import { useAppStore } from '../../stores/app.js'
const store = useAppStore()
const profile = computed(() => store.userProfile || store.registrationData || {})
const eventsCount = computed(() => store.events?.length || 0)
const scriptsCount = computed(() => store.scripts?.length || 0)
const avatarText = computed(() => (profile.value.nickname || 'Z').slice(0, 1))
const chips = computed(() => [profile.value.zodiac, profile.value.mbti, profile.value.profession].filter(Boolean))
const hobbyTags = computed(() => {
const hobbies = profile.value.hobbies
if (Array.isArray(hobbies) && hobbies.length) return hobbies
return ['阅读', '旅行', '音乐', '创作']
})
const signature = computed(() => profile.value.future?.ideal || '正在把人生整理成一份会发光的档案。')
const editProfile = () => {
uni.navigateTo({ url: '/pages/onboarding/index?edit=1' })
}
const handleLogout = () => {
uni.showModal({
title: '退出登录',
content: '确定要离开当前数字生命档案吗?',
success: async (res) => {
if (res.confirm) {
await store.logout()
uni.reLaunch({ url: '/pages/login/index' })
}
}
})
}
</script>
<style scoped>
.mine-view {
display: flex;
flex-direction: column;
gap: 24rpx;
padding-bottom: 32rpx;
}
.profile-card {
border-radius: 34rpx;
padding: 32rpx;
display: flex;
align-items: center;
gap: 22rpx;
}
.avatar {
width: 112rpx;
height: 112rpx;
flex-shrink: 0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 50rpx;
font-weight: 900;
background: linear-gradient(135deg, #b245ff, #2a7dff);
box-shadow: 0 0 36rpx rgba(168, 85, 255, 0.55);
}
.profile-main {
flex: 1;
min-width: 0;
}
.name-row {
display: flex;
align-items: center;
gap: 10rpx;
}
.name {
color: #fff;
font-size: 38rpx;
font-weight: 900;
}
.star {
color: #ffd184;
}
.signature {
display: block;
margin-top: 10rpx;
color: rgba(224, 211, 246, 0.66);
font-size: 24rpx;
line-height: 1.5;
}
.chips,
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 18rpx;
}
.chip,
.tag {
height: 44rpx;
padding: 0 16rpx;
border-radius: 999rpx;
display: inline-flex;
align-items: center;
color: rgba(244, 235, 255, 0.86);
font-size: 21rpx;
}
.edit-btn {
align-self: flex-start;
height: 54rpx;
padding: 0 18rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
color: #caa0ff;
font-size: 22rpx;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18rpx;
}
.stat-card {
border-radius: 28rpx;
padding: 28rpx;
}
.stat-value {
display: block;
color: #fff;
font-size: 48rpx;
font-weight: 900;
}
.stat-label {
display: block;
margin-top: 6rpx;
color: rgba(224, 211, 246, 0.62);
font-size: 23rpx;
}
.section-card {
border-radius: 30rpx;
padding: 30rpx;
}
.section-title {
color: #fff;
font-size: 30rpx;
font-weight: 900;
}
.memory-line {
display: grid;
grid-template-columns: 86rpx 1fr;
gap: 18rpx;
padding: 22rpx 0;
border-bottom: 1rpx solid rgba(180, 139, 255, 0.14);
}
.memory-line:last-child {
border-bottom: 0;
}
.memory-label {
color: #b56cff;
font-size: 24rpx;
font-weight: 800;
}
.memory-text {
color: rgba(224, 211, 246, 0.72);
font-size: 24rpx;
line-height: 1.55;
}
.logout {
height: 72rpx;
border-radius: 999rpx;
color: rgba(224, 211, 246, 0.74);
font-size: 25rpx;
}
</style>
File diff suppressed because it is too large Load Diff
+298 -200
View File
@@ -1,265 +1,363 @@
<template>
<view class="detail-view">
<!-- 状态栏占位 -->
<view :style="{ height: statusBarHeight + 'px' }" class="status-bar-placeholder"></view>
<view class="detail-page">
<view class="space-bg"></view>
<view class="status-space" :style="{ height: statusBarHeight + 'px' }"></view>
<view class="detail-header">
<view class="header-left" @click="goBack">
<text class="back-icon"></text>
<text class="back-text">返回</text>
</view>
<text class="detail-title">{{ script?.title || '剧本详情' }}</text>
<view class="topbar">
<button class="back-btn" @click="goBack"></button>
<text class="top-title">人生剧本 </text>
<button class="save-btn kos-pill" @click="selectCurrent">映射</button>
</view>
<scroll-view class="detail-content" scroll-y>
<view class="content-container">
<!-- 基本信息卡片 -->
<view class="info-card glass-card">
<view class="info-row">
<text class="info-label">主题</text>
<text class="info-value">{{ script?.theme || '-' }}</text>
<scroll-view class="scroll" scroll-y :show-scrollbar="false">
<view class="hero-card kos-card">
<text class="eyebrow">PARALLEL LIFE</text>
<text class="script-title">{{ script?.title || '未命名剧本' }}</text>
<text class="script-summary">{{ script?.summary || '命运正在整理章节。' }}</text>
<view class="stats">
<view class="stat">
<text class="stat-value">{{ script?.style || '爽文' }}</text>
<text class="stat-label">风格</text>
</view>
<view class="info-row">
<text class="info-label">风格</text>
<text class="info-value">{{ script?.style || '-' }}</text>
<view class="stat">
<text class="stat-value">{{ lengthText }}</text>
<text class="stat-label">篇幅</text>
</view>
<view class="info-row">
<text class="info-label">篇幅</text>
<text class="info-value">{{ script?.length || '-' }}</text>
<view class="stat">
<text class="stat-value">{{ script?.wordCount || 0 }}</text>
<text class="stat-label">字数</text>
</view>
</view>
</view>
<!-- 完整内容 -->
<view class="full-content glass-card">
<view class="content-header">
<text class="content-icon">📖</text>
<text class="content-label">完整剧本</text>
<view class="tabs kos-card">
<view class="tab" :class="{ active: activeTab === 'content' }" @click="activeTab = 'content'">正文</view>
<view class="tab" :class="{ active: activeTab === 'outline' }" @click="activeTab = 'outline'">大纲</view>
</view>
<view v-if="activeTab === 'content'" class="article-card kos-card">
<Markdown :content="fullContent" />
</view>
<view v-else class="outline-list">
<view v-for="(item, index) in outline" :key="index" class="outline-card kos-card">
<view class="outline-node">{{ index + 1 }}</view>
<view class="outline-body">
<text class="outline-title">{{ item.title }}</text>
<text class="outline-text">{{ item.text }}</text>
</view>
<Markdown :content="fullContent" />
</view>
</view>
</scroll-view>
<view class="bottom-actions">
<button class="secondary-btn kos-pill" @click="goBack">返回列表</button>
<button class="primary-btn kos-primary" @click="selectCurrent">映射实现路径</button>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
import Markdown from '../../components/Markdown.vue'
const store = useAppStore()
const statusBarHeight = ref(20)
const activeTab = ref('content')
const scriptId = ref('')
const script = ref(null)
const fullContent = ref('')
const statusBarHeight = ref(0)
const scripts = computed(() => store.scripts || [])
onMounted(() => {
// 获取状态栏高度(从 App.vue 存储的全局值)
statusBarHeight.value = uni.getStorageSync('statusBarHeight') || 20
// 获取剧本 ID
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const scriptId = currentPage.options.id
// 从 store 获取剧本详情
if (scriptId) {
script.value = scripts.value.find(s => s.id === scriptId)
// 优先读取 content 字段(转换后的格式),兼容 plotJson.fullContent
if (script.value?.content) {
fullContent.value = script.value.content
} else if (script.value?.plotJson?.fullContent) {
fullContent.value = script.value.plotJson.fullContent
} else {
fullContent.value = '暂无完整内容'
}
}
const fullContent = computed(() => script.value?.content || '暂无正文内容。')
const lengthText = computed(() => {
const map = { short: '短篇', medium: '中篇', long: '长篇' }
return map[script.value?.length] || script.value?.length || '中篇'
})
const outline = computed(() => {
const text = fullContent.value
const parts = text.split(/\n{2,}/).filter(Boolean)
if (parts.length > 1) {
return parts.slice(0, 8).map((part, index) => {
const clean = part.replace(/[#>*_`]/g, '').trim()
const lines = clean.split('\n').filter(Boolean)
return {
title: lines[0]?.replace(/[【】]/g, '') || `章节 ${index + 1}`,
text: lines.slice(1).join(' ').slice(0, 120) || clean.slice(0, 120)
}
})
}
return [
{ title: '起点', text: script.value?.theme || '从真实的人生经验出发。' },
{ title: '转折', text: '关键机会出现,旧有困境开始松动。' },
{ title: '高光', text: '主角完成一次真正的选择,并看见新的自我。' },
{ title: '回响', text: '剧本落回现实,转化为可执行路径。' }
]
})
const loadScript = async () => {
if (!scriptId.value) return
script.value = store.getScriptById(scriptId.value)
if (!script.value) {
await store.fetchScripts()
script.value = store.getScriptById(scriptId.value)
}
}
const selectCurrent = async () => {
if (!script.value?.id) return
const res = await store.selectScript(script.value.id)
if (!res.success) {
uni.showToast({ title: res.error || '映射失败', icon: 'none' })
return
}
uni.navigateTo({ url: '/pages/main/PathView' })
}
const goBack = () => {
uni.navigateBack()
}
onMounted(async () => {
statusBarHeight.value = uni.getStorageSync('statusBarHeight') || 20
const pages = getCurrentPages()
scriptId.value = pages[pages.length - 1]?.options?.id || ''
await loadScript()
})
</script>
<style scoped>
.detail-view {
display: flex;
flex-direction: column;
height: 100%;
background: linear-gradient(180deg, #0F071A 0%, #1A0B2E 50%, #0F071A 100%);
}
/* 状态栏占位 */
.status-bar-placeholder {
width: 100%;
flex-shrink: 0;
background: transparent;
}
/* ==================== 顶部导航栏 ==================== */
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 32rpx 24rpx;
background: rgba(168, 85, 247, 0.1);
border-bottom: 1px solid rgba(168, 85, 247, 0.2);
flex-shrink: 0;
.detail-page {
position: relative;
height: 100vh;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
color: #fff;
background: #050615;
}
.header-left {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 16rpx;
border-radius: 24rpx;
background: rgba(168, 85, 247, 0.15);
border: 1px solid rgba(168, 85, 247, 0.2);
z-index: 10;
.space-bg {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 18% 4%, rgba(129, 67, 255, 0.28), transparent 30%),
radial-gradient(circle at 90% 16%, rgba(54, 122, 255, 0.16), transparent 28%),
linear-gradient(180deg, #07091d 0%, #07031a 52%, #04020e 100%);
}
.status-space,
.topbar,
.scroll,
.bottom-actions {
position: relative;
z-index: 1;
}
.status-space,
.topbar,
.bottom-actions {
flex-shrink: 0;
}
.back-icon {
font-size: 32rpx;
color: #C084FC;
font-weight: 600;
.topbar {
height: 92rpx;
display: grid;
grid-template-columns: 86rpx 1fr 86rpx;
align-items: center;
padding: 0 30rpx;
}
.back-text {
font-size: 24rpx;
color: rgba(243, 232, 255, 0.8);
.back-btn {
color: #fff;
font-size: 64rpx;
text-align: left;
}
.detail-title {
font-size: 32rpx;
font-weight: 500;
color: rgba(243, 232, 255, 0.9);
font-family: 'Cinzel', 'Inter', serif;
flex: 1;
.top-title {
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-left: 8rpx;
margin-right: 8rpx;
color: #fff;
font-size: 34rpx;
font-weight: 900;
}
/* ==================== 滚动内容区 ==================== */
.detail-content {
.save-btn {
height: 54rpx;
border-radius: 999rpx;
color: #caa0ff;
font-size: 23rpx;
}
.scroll {
flex: 1;
overflow-y: auto;
height: 0;
min-height: 0;
box-sizing: border-box;
padding: 0 30rpx;
}
.content-container {
.hero-card {
border-radius: 32rpx;
padding: 36rpx 30rpx;
}
.eyebrow {
display: block;
color: #c186ff;
font-size: 18rpx;
letter-spacing: 6rpx;
}
.script-title {
display: block;
margin-top: 14rpx;
color: #fff;
font-size: 44rpx;
line-height: 1.18;
font-weight: 900;
}
.script-summary {
display: block;
margin-top: 18rpx;
color: rgba(226, 216, 246, 0.7);
font-size: 25rpx;
line-height: 1.62;
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
margin-top: 28rpx;
}
.stat {
padding: 18rpx 10rpx;
border-radius: 20rpx;
text-align: center;
background: rgba(255, 255, 255, 0.06);
}
.stat-value,
.stat-label {
display: block;
}
.stat-value {
color: #fff;
font-size: 24rpx;
font-weight: 800;
}
.stat-label {
margin-top: 6rpx;
color: rgba(219, 207, 243, 0.58);
font-size: 19rpx;
}
.tabs {
height: 76rpx;
margin-top: 24rpx;
border-radius: 999rpx;
padding: 6rpx;
display: grid;
grid-template-columns: 1fr 1fr;
}
.tab {
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(224, 214, 243, 0.64);
font-size: 26rpx;
font-weight: 700;
}
.tab.active {
color: #fff;
background: linear-gradient(135deg, #a63cff, #6530ff);
box-shadow: 0 0 26rpx rgba(162, 71, 255, 0.48);
}
.article-card {
margin: 24rpx 0 34rpx;
border-radius: 30rpx;
padding: 30rpx;
}
.outline-list {
display: flex;
flex-direction: column;
gap: 24rpx;
/* scroll-view 在小程序中不适合直接承担内容留白,改由内部容器控制 */
padding: 24rpx 32rpx 32rpx;
box-sizing: border-box;
gap: 18rpx;
margin: 24rpx 0 34rpx;
}
/* ==================== 信息卡片 ==================== */
.info-card {
padding: 32rpx;
background: linear-gradient(135deg, rgba(168, 85, 247, 0.12), rgba(232, 121, 249, 0.08));
border: 1px solid rgba(168, 85, 247, 0.25);
border-radius: 40rpx;
box-shadow: inset 0 0 20rpx rgba(168, 85, 247, 0.05),
0 4rpx 20rpx rgba(168, 85, 247, 0.08);
}
.info-row {
.outline-card {
border-radius: 26rpx;
padding: 26rpx;
display: flex;
gap: 20rpx;
}
.outline-node {
width: 48rpx;
height: 48rpx;
flex-shrink: 0;
border-radius: 50%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
border-bottom: 1px solid rgba(168, 85, 247, 0.1);
justify-content: center;
color: #fff;
font-size: 22rpx;
font-weight: 900;
background: linear-gradient(135deg, #a855ff, #4f46e5);
box-shadow: 0 0 22rpx rgba(168, 85, 255, 0.48);
}
.outline-body {
flex: 1;
min-width: 0;
}
.info-row:last-child {
border-bottom: none;
.outline-title {
display: block;
color: #fff;
font-size: 27rpx;
font-weight: 900;
}
.info-label {
font-size: 22rpx;
color: rgba(192, 132, 252, 0.7);
font-weight: 600;
letter-spacing: 2rpx;
text-transform: uppercase;
flex-shrink: 0;
.outline-text {
display: block;
margin-top: 10rpx;
color: rgba(223, 211, 245, 0.7);
font-size: 24rpx;
line-height: 1.6;
}
.info-value {
flex: 1;
text-align: right;
margin-left: 16rpx;
overflow: hidden;
word-break: break-all;
color: rgba(243, 232, 255, 0.9);
.bottom-actions {
height: 132rpx;
padding: 16rpx 30rpx 26rpx;
box-sizing: border-box;
display: grid;
grid-template-columns: 1fr 1.45fr;
gap: 18rpx;
background: rgba(5, 6, 21, 0.72);
backdrop-filter: blur(24rpx);
}
.secondary-btn,
.primary-btn {
height: 82rpx;
border-radius: 999rpx;
font-size: 26rpx;
line-height: 1.5;
font-weight: 800;
}
/* ==================== 完整内容卡片 ==================== */
.full-content {
padding: 40rpx 32rpx;
background: rgba(168, 85, 247, 0.08);
border: 1px solid rgba(168, 85, 247, 0.2);
border-radius: 40rpx;
min-height: 400rpx;
overflow: hidden;
word-break: break-all;
}
.content-header {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 32rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid rgba(168, 85, 247, 0.2);
}
.content-icon {
font-size: 36rpx;
filter: drop-shadow(0 0 8rpx rgba(192, 132, 252, 0.6));
}
.content-label {
font-size: 26rpx;
color: rgba(192, 132, 252, 0.8);
font-weight: 600;
letter-spacing: 4rpx;
text-transform: uppercase;
}
/* Markdown 内容间距调整 */
.full-content .markdown-container {
gap: 24rpx;
}
.full-content .markdown-h3 {
margin-top: 32rpx;
margin-bottom: 16rpx;
font-size: 32rpx;
}
.full-content .markdown-h4 {
margin-top: 24rpx;
margin-bottom: 12rpx;
}
.full-content .markdown-p {
font-size: 26rpx;
line-height: 1.8;
color: rgba(243, 232, 255, 0.85);
}
.full-content .markdown-hr {
margin: 40rpx 0;
.secondary-btn {
color: #caa0ff;
}
</style>
File diff suppressed because it is too large Load Diff
+202 -285
View File
@@ -1,364 +1,281 @@
<template>
<view class="main-page">
<!-- 星空背景 -->
<view class="stars-container">
<view
v-for="star in stars"
:key="star.id"
class="star"
:style="{
left: star.left + '%',
top: star.top + '%',
width: star.size + 'px',
height: star.size + 'px',
'--opacity': star.opacity,
'--x': star.xMove + 'px',
'--y': star.yMove + 'px',
'--duration': star.duration + 's',
'--delay': star.delay + 's'
}"
></view>
<view class="app-shell">
<view class="space-bg">
<view class="nebula nebula-a"></view>
<view class="nebula nebula-b"></view>
<view class="stars"></view>
</view>
<view class="bg-decoration">
<view class="aurora-top"></view>
<view class="aurora-bottom"></view>
</view>
<view class="safe-top" :style="{ height: safeAreaTop + 14 + 'px' }"></view>
<view class="header" :style="{ paddingTop: safeAreaTop + 20 + 'px' }">
<view class="header-left">
<view class="logo-box">
<image class="logo" src="/static/logo.svg" mode="aspectFit" />
</view>
<view class="brand">
<text class="brand-title font-serif">人生 OS</text>
<text class="brand-subtitle">LIFE HARMONY v3.1</text>
</view>
</view>
<view class="header-right" @click="goToProfile">
<view class="avatar-box">
<image class="avatar" :src="userAvatar" mode="aspectFill" />
</view>
</view>
</view>
<scroll-view class="page-content" scroll-y>
<view v-if="activeTab === 'record'" class="tab-page">
<RecordView></RecordView>
</view>
<view v-if="activeTab === 'script'" class="tab-page">
<ScriptView></ScriptView>
</view>
<view v-if="activeTab === 'path'" class="tab-page">
<PathView></PathView>
</view>
<scroll-view class="content" scroll-y :enhanced="true" :show-scrollbar="false">
<RecordView v-if="activeTab === 'record'" />
<ScriptView v-if="activeTab === 'script'" />
<MineView v-if="activeTab === 'mine'" />
</scroll-view>
<!-- 全局音乐播放器 -->
<MusicPlayer ref="musicPlayer" />
<view class="bottom-nav" :style="{ paddingBottom: safeAreaBottom + 'px' }">
<view
class="nav-item"
:class="{ active: activeTab === 'record' }"
@click="switchTab('record')"
>
<text class="nav-icon">📖</text>
<text class="nav-label">回溯过去</text>
</view>
<view
class="nav-item"
:class="{ active: activeTab === 'script' }"
@click="switchTab('script')"
>
<text class="nav-icon"></text>
<text class="nav-label">创造未来</text>
</view>
<view
class="nav-item"
:class="{ active: activeTab === 'path' }"
@click="switchTab('path')"
>
<text class="nav-icon">🗺</text>
<text class="nav-label">路径实现</text>
<view class="bottom-nav">
<view class="nav-inner" :style="{ paddingBottom: safeAreaBottom + 'px' }">
<view class="nav-item" :class="{ active: activeTab === 'record' }" @click="switchTab('record')">
<view class="planet-icon">
<view class="planet-core"></view>
</view>
<text>人生轨迹</text>
</view>
<view class="nav-item" :class="{ active: activeTab === 'script' }" @click="switchTab('script')">
<view class="book-icon">
<view></view>
<view></view>
</view>
<text>爽文生成</text>
</view>
<view class="nav-item" :class="{ active: activeTab === 'mine' }" @click="switchTab('mine')">
<view class="smile-icon">
<view class="eye left"></view>
<view class="eye right"></view>
</view>
<text>我的</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { useAppStore } from '../../stores/app.js'
import RecordView from './RecordView.vue'
import ScriptView from './ScriptView.vue'
import PathView from './PathView.vue'
import MineView from './MineView.vue'
import MusicPlayer from '../../components/MusicPlayer.vue'
const store = useAppStore()
const activeTab = ref('record')
const musicPlayer = ref(null)
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
const safeAreaBottom = ref(uni.getStorageSync('safeAreaBottom') || 0)
// 星空背景数据
const stars = ref([])
const initStars = () => {
stars.value = []
for (let i = 0; i < 60; i++) {
stars.value.push({
id: i,
size: Math.random() * 3 + 1,
left: Math.random() * 100,
top: Math.random() * 100,
opacity: 0.2 + Math.random() * 0.5,
xMove: (Math.random() - 0.5) * 100,
yMove: (Math.random() - 0.5) * 100,
duration: 15 + Math.random() * 20,
delay: Math.random() * -20
})
}
}
onMounted(async () => {
// 使用新的推荐 API 替代已弃用的 getSystemInfoSync
const windowInfo = uni.getWindowInfo()
safeAreaTop.value = windowInfo.safeAreaInsets?.top || windowInfo.statusBarHeight || 20
safeAreaBottom.value = windowInfo.safeAreaInsets?.bottom || 0
store.initialize()
uni.$on('switchTab', switchTab)
initStars()
// 预加载所有 Tab 的数据,确保首次进入页面时数据已就绪
await Promise.all([
store.fetchEvents(),
store.fetchScripts(),
store.fetchPaths()
])
})
const userAvatar = computed(() => {
const nickname = store.userProfile?.nickname || 'user'
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${nickname}&backgroundColor=A855F7`
})
const switchTab = (tab) => {
activeTab.value = tab
}
onMounted(async () => {
const windowInfo = uni.getWindowInfo()
safeAreaTop.value = windowInfo.safeAreaInsets?.top || windowInfo.statusBarHeight || 20
safeAreaBottom.value = windowInfo.safeAreaInsets?.bottom || 0
const session = await store.restoreSession()
if (session.status !== store.SESSION_STATUS.AUTHENTICATED) {
uni.reLaunch({ url: '/pages/login/index' })
return
}
uni.$on('switchTab', switchTab)
await Promise.all([
store.fetchUserProfile(),
store.fetchEvents(),
store.fetchScripts(),
store.fetchPaths(),
store.fetchInspirationRecommendations()
])
})
onUnmounted(() => {
uni.$off('switchTab', switchTab)
})
const goToProfile = () => {
uni.navigateTo({ url: '/pages/profile/index' })
}
</script>
<style scoped>
.main-page {
height: 100vh;
background: linear-gradient(180deg, #0F071A 0%, #1A0B2E 50%, #0F071A 100%);
.app-shell {
position: relative;
height: 100vh;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
overflow: hidden;
background: #050615;
color: #fff;
}
/* 星空背景 */
.stars-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
}
.star {
position: absolute;
background: white;
border-radius: 50%;
opacity: var(--opacity);
animation: float-star var(--duration) ease-in-out infinite;
animation-delay: var(--delay);
}
@keyframes float-star {
0%, 100% {
transform: translate(0, 0) scale(1);
opacity: var(--opacity);
}
50% {
transform: translate(var(--x), var(--y)) scale(1.5);
opacity: 1;
}
}
.bg-decoration {
.space-bg {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 1;
overflow: hidden;
background:
radial-gradient(circle at 82% 16%, rgba(100, 50, 255, 0.28), transparent 26%),
radial-gradient(circle at 0% 58%, rgba(36, 134, 255, 0.2), transparent 28%),
linear-gradient(180deg, #07091d 0%, #08031a 48%, #04020e 100%);
}
.aurora-top {
.nebula {
position: absolute;
top: -10%;
left: -10%;
width: 120%;
height: 60%;
background: rgba(168, 85, 247, 0.08);
filter: blur(120rpx);
border-radius: 50%;
filter: blur(20rpx);
opacity: 0.9;
}
.aurora-bottom {
.nebula-a {
width: 420rpx;
height: 420rpx;
right: -170rpx;
top: 160rpx;
background: radial-gradient(circle, rgba(144, 67, 255, 0.38), transparent 66%);
}
.nebula-b {
width: 520rpx;
height: 520rpx;
left: -260rpx;
bottom: 100rpx;
background: radial-gradient(circle, rgba(30, 104, 255, 0.18), transparent 70%);
}
.stars {
position: absolute;
bottom: -10%;
right: -10%;
width: 100%;
height: 50%;
background: rgba(139, 92, 246, 0.05);
filter: blur(100rpx);
border-radius: 50%;
inset: 0;
opacity: 0.48;
background-image:
radial-gradient(circle, rgba(255,255,255,0.82) 0 1rpx, transparent 2rpx),
radial-gradient(circle, rgba(178,128,255,0.52) 0 1rpx, transparent 2rpx),
radial-gradient(circle, rgba(255,202,120,0.8) 0 1rpx, transparent 2rpx);
background-size: 122rpx 138rpx, 184rpx 164rpx, 246rpx 220rpx;
background-position: 20rpx 0, 60rpx 30rpx, 16rpx 80rpx;
}
.header {
.safe-top {
position: relative;
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
padding-top: calc(24rpx + constant(safe-area-inset-top));
padding-top: calc(24rpx + env(safe-area-inset-top));
background: rgba(15, 7, 26, 0.6);
backdrop-filter: blur(20rpx);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
z-index: 2;
flex-shrink: 0;
}
.header-left {
display: flex;
align-items: center;
gap: 16rpx;
}
.logo-box {
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, #9333EA 0%, #7C3AED 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.logo {
width: 64rpx;
height: 64rpx;
}
.brand-title {
display: block;
font-size: 26rpx;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
letter-spacing: 6rpx;
}
.brand-subtitle {
display: block;
font-size: 16rpx;
color: rgba(168, 85, 247, 0.6);
letter-spacing: 4rpx;
}
.avatar-box {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
overflow: hidden;
}
.avatar {
width: 100%;
height: 100%;
}
.page-content {
.content {
position: relative;
z-index: 1;
flex: 1;
position: relative;
z-index: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
height: 0;
}
.tab-page {
padding: 32rpx;
padding-bottom: calc(180rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
min-height: 0;
box-sizing: border-box;
padding: 0 28rpx 156rpx;
}
.bottom-nav {
position: fixed;
bottom: 0;
position: absolute;
left: 0;
right: 0;
height: 140rpx;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
background: rgba(15, 7, 26, 0.95);
backdrop-filter: blur(40rpx);
-webkit-backdrop-filter: blur(40rpx);
border-top: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
justify-content: space-around;
bottom: 0;
z-index: 20;
box-sizing: border-box;
padding-left: 28rpx;
padding-right: 28rpx;
}
.nav-inner {
height: 108rpx;
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: center;
z-index: 100;
border-radius: 34rpx;
border: 1rpx solid rgba(153, 112, 255, 0.32);
background: rgba(11, 9, 35, 0.84);
box-shadow: inset 0 0 38rpx rgba(129, 65, 255, 0.12), 0 18rpx 70rpx rgba(0, 0, 0, 0.36);
backdrop-filter: blur(26rpx);
-webkit-backdrop-filter: blur(26rpx);
}
.nav-item {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6rpx;
padding: 16rpx 32rpx;
color: rgba(255, 255, 255, 0.4);
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
min-width: 160rpx;
gap: 8rpx;
color: rgba(216, 208, 235, 0.58);
font-size: 23rpx;
font-weight: 600;
transition: transform 0.22s ease, color 0.22s ease;
}
.nav-item.active {
color: #C084FC;
transform: translateY(-8rpx);
text-shadow: 0 0 15px rgba(168, 85, 247, 0.6);
color: #b86cff;
transform: translateY(-4rpx);
text-shadow: 0 0 22rpx rgba(178, 91, 255, 0.8);
}
.nav-icon {
font-size: 40rpx;
.planet-icon,
.book-icon,
.smile-icon {
position: relative;
width: 46rpx;
height: 42rpx;
color: currentColor;
}
.nav-label {
font-size: 18rpx;
font-weight: 600;
letter-spacing: 4rpx;
text-transform: uppercase;
.planet-core {
position: absolute;
left: 10rpx;
top: 8rpx;
width: 26rpx;
height: 26rpx;
border-radius: 50%;
background: currentColor;
box-shadow: 0 0 24rpx currentColor;
}
.safe-area-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
.planet-icon::before {
content: '';
position: absolute;
left: 2rpx;
top: 14rpx;
width: 42rpx;
height: 14rpx;
border: 4rpx solid currentColor;
border-top-color: transparent;
border-radius: 50%;
transform: rotate(-22deg);
}
.book-icon {
display: flex;
gap: 4rpx;
justify-content: center;
align-items: center;
}
.book-icon view {
width: 17rpx;
height: 34rpx;
border-radius: 6rpx 3rpx 3rpx 6rpx;
border: 4rpx solid currentColor;
box-shadow: 0 0 18rpx currentColor;
}
.smile-icon {
border-radius: 50%;
border: 5rpx solid currentColor;
box-sizing: border-box;
}
.eye {
position: absolute;
top: 12rpx;
width: 6rpx;
height: 6rpx;
border-radius: 50%;
background: currentColor;
}
.eye.left { left: 12rpx; }
.eye.right { right: 12rpx; }
.smile-icon::after {
content: '';
position: absolute;
left: 12rpx;
right: 12rpx;
bottom: 10rpx;
height: 8rpx;
border-bottom: 4rpx solid currentColor;
border-radius: 0 0 20rpx 20rpx;
}
</style>
File diff suppressed because it is too large Load Diff
+32 -285
View File
@@ -1,314 +1,61 @@
<template>
<view class="profile-page">
<view class="bg-decoration">
<view class="aurora-top"></view>
<view class="aurora-bottom"></view>
<view class="top-safe" :style="{ height: safeAreaTop + 'px' }"></view>
<view class="header">
<text class="back" @click="goBack"></text>
<text class="title">个人中心</text>
<text></text>
</view>
<scroll-view class="content" scroll-y :style="{ paddingTop: safeAreaTop + 20 + 'px', paddingBottom: safeAreaBottom + 20 + 'px' }">
<view class="user-card">
<view class="avatar-box">
<image class="avatar" :src="userAvatar" mode="aspectFill" />
<view class="verified-badge"></view>
</view>
<view class="user-info">
<text class="nickname font-serif">{{ userProfile.nickname || '未同步系统' }}</text>
<text class="user-tags">{{ userTags }}</text>
</view>
</view>
<view class="stats-row">
<view class="stat-card glass-card">
<text class="stat-label">觉醒深度</text>
<text class="stat-value">Lv.4</text>
</view>
<view class="stat-card glass-card">
<text class="stat-label">星历契合</text>
<text class="stat-value">98%</text>
</view>
</view>
<view class="menu-list">
<view class="menu-item glass-card" @click="editProfile">
<view class="menu-left">
<text class="menu-icon">👤</text>
<text class="menu-title">个人档案设置</text>
</view>
<text class="menu-arrow"></text>
</view>
<view class="menu-item glass-card">
<view class="menu-left">
<text class="menu-icon">🔄</text>
<text class="menu-title">多账号切换</text>
</view>
<text class="menu-arrow"></text>
</view>
<view class="menu-item glass-card">
<view class="menu-left">
<text class="menu-icon">📧</text>
<text class="menu-title">与开发者对话</text>
</view>
<text class="menu-arrow"></text>
</view>
</view>
<button class="logout-btn" @click="handleLogout">
TERMINATE LIFE HARMONY
</button>
</scroll-view>
<MineView class="content" />
</view>
</template>
<script setup>
import { computed, ref, onMounted } from 'vue'
import { useAppStore } from '../../stores/app.js'
import { onMounted, ref } from 'vue'
import MineView from '../main/MineView.vue'
const store = useAppStore()
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
const safeAreaBottom = ref(uni.getStorageSync('safeAreaBottom') || 0)
const safeAreaTop = ref(20)
onMounted(() => {
// 使用新的推荐 API 替代已弃用的 getSystemInfoSync
const windowInfo = uni.getWindowInfo()
safeAreaTop.value = windowInfo.safeAreaInsets?.top || windowInfo.statusBarHeight || 20
safeAreaBottom.value = windowInfo.safeAreaInsets?.bottom || 0
const info = uni.getWindowInfo()
safeAreaTop.value = info.safeAreaInsets?.top || info.statusBarHeight || 20
})
const userProfile = computed(() => store.userProfile || {})
const userAvatar = computed(() => {
const nickname = userProfile.value.nickname || 'user'
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${nickname}&backgroundColor=A855F7`
})
const userTags = computed(() => {
const { mbti, zodiac, profession } = userProfile.value
const tags = [mbti || 'QUESTER', zodiac || 'STAR', profession || '星民']
return tags.join(' · ')
})
const editProfile = () => {
uni.navigateTo({ url: '/pages/onboarding/index?edit=1' })
}
const handleLogout = () => {
uni.showModal({
title: '确认退出',
content: '确定要退出登录吗?',
success: async (res) => {
if (res.confirm) {
await store.logout()
uni.reLaunch({ url: '/pages/login/index' })
}
}
})
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
.profile-page {
min-height: 100vh;
background: linear-gradient(180deg, #0F071A 0%, #1A0B2E 50%, #0F071A 100%);
position: relative;
color: #fff;
background:
radial-gradient(circle at 18% 2%, rgba(124, 58, 237, 0.3), transparent 34%),
linear-gradient(180deg, #090514 0%, #1a0a2f 55%, #07020d 100%);
}
.bg-decoration {
position: absolute;
inset: 0;
pointer-events: none;
.header {
height: 92rpx;
display: grid;
grid-template-columns: 80rpx 1fr 80rpx;
align-items: center;
padding: 0 28rpx;
}
.aurora-top {
position: absolute;
top: -10%;
left: -10%;
width: 120%;
height: 60%;
background: rgba(168, 85, 247, 0.08);
filter: blur(120rpx);
border-radius: 50%;
.back {
font-size: 62rpx;
color: rgba(255, 255, 255, 0.82);
}
.aurora-bottom {
position: absolute;
bottom: -10%;
right: -10%;
width: 100%;
height: 50%;
background: rgba(139, 92, 246, 0.05);
filter: blur(100rpx);
border-radius: 50%;
.title {
text-align: center;
font-size: 32rpx;
font-weight: 800;
}
.content {
position: relative;
z-index: 1;
padding: 40rpx;
padding-top: calc(60rpx + constant(safe-area-inset-top));
padding-top: calc(60rpx + env(safe-area-inset-top));
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
.user-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 48rpx 0;
margin-bottom: 32rpx;
}
.avatar-box {
position: relative;
width: 160rpx;
height: 160rpx;
border-radius: 50%;
/* 原型标准:强发光边框 */
border: 4rpx solid rgba(168, 85, 247, 0.3);
box-shadow: 0 0 40px rgba(168, 85, 247, 0.1),
inset 0 0 20rpx rgba(168, 85, 247, 0.05);
padding: 8rpx;
margin-bottom: 32rpx;
background: rgba(168, 85, 247, 0.1);
}
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(168, 85, 247, 0.1);
}
.verified-badge {
position: absolute;
bottom: 8rpx;
right: 8rpx;
width: 40rpx;
height: 40rpx;
background: #9333EA;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
color: white;
border: 4rpx solid #0F071A;
}
.user-info {
text-align: center;
}
.nickname {
display: block;
font-size: 40rpx;
font-weight: 300;
color: rgba(255, 255, 255, 0.95);
margin-bottom: 16rpx;
letter-spacing: 4rpx;
}
.user-tags {
display: block;
font-size: 18rpx;
color: rgba(168, 85, 247, 0.6);
letter-spacing: 4rpx;
text-transform: uppercase;
}
.stats-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24rpx;
margin-bottom: 48rpx;
}
.stat-card {
padding: 32rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
/* 原型标准:玻璃态效果 */
background: rgba(168, 85, 247, 0.05);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(168, 85, 247, 0.15);
border-radius: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(168, 85, 247, 0.05);
transition: all 0.3s ease;
}
.stat-card:active {
transform: scale(0.98);
box-shadow: 0 2rpx 12rpx rgba(168, 85, 247, 0.03);
}
.stat-label {
font-size: 18rpx;
color: rgba(255, 255, 255, 0.35);
letter-spacing: 4rpx;
text-transform: uppercase;
}
.stat-value {
font-size: 36rpx;
font-weight: 300;
color: rgba(243, 232, 255, 0.9);
}
.menu-list {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-bottom: 64rpx;
}
.menu-item {
padding: 32rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.menu-left {
display: flex;
align-items: center;
gap: 24rpx;
}
.menu-icon {
font-size: 32rpx;
}
.menu-title {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
}
.menu-arrow {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.3);
}
.logout-btn {
width: 100%;
padding: 32rpx;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 24rpx;
color: rgba(255, 255, 255, 0.3);
font-size: 20rpx;
letter-spacing: 6rpx;
text-transform: uppercase;
}
.logout-btn:active {
color: rgba(168, 85, 247, 0.5);
border-color: rgba(168, 85, 247, 0.2);
padding: 24rpx 32rpx 40rpx;
}
</style>
+21 -13
View File
@@ -16,31 +16,39 @@
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { onMounted, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
import { logRuntimeEnv } from '../../services/request.js'
const statusBarHeight = ref(20)
const safeAreaBottom = ref(0)
onLoad(() => {
onMounted(() => {
logRuntimeEnv('splash:onLoad')
// 使用新的推荐 API 替代已弃用的 getSystemInfoSync
const windowInfo = uni.getWindowInfo()
statusBarHeight.value = windowInfo.statusBarHeight || 20
safeAreaBottom.value = windowInfo.safeAreaInsets?.bottom || 0
setTimeout(async () => {
const store = useAppStore()
const token = uni.getStorageSync('access_token')
if (token) {
await store.fetchUserProfile()
if (store.hasProfile) {
uni.reLaunch({ url: '/pages/main/index' })
} else {
uni.reLaunch({ url: '/pages/onboarding/index' })
}
} else {
uni.reLaunch({ url: '/pages/login/index' })
const session = await store.restoreSession()
if (session.status === store.SESSION_STATUS.AUTHENTICATED) {
const target = session.hasProfile ? '/pages/main/index' : '/pages/onboarding/index'
console.log('[AUTH] route', { target, reason: session.reason, hasProfile: session.hasProfile })
uni.reLaunch({ url: target })
return
}
if (session.status === store.SESSION_STATUS.ERROR) {
console.log('[AUTH] route', { target: '/pages/login/index', reason: session.reason })
uni.showToast({ title: '服务连接异常,请稍后重试', icon: 'none' })
uni.reLaunch({ url: '/pages/login/index' })
return
}
console.log('[AUTH] route', { target: '/pages/login/index', reason: session.reason })
uni.reLaunch({ url: '/pages/login/index' })
}, 2000)
})
</script>
+6
View File
@@ -93,6 +93,11 @@ export const validateToken = async () => {
}
}
export const validateTokenStrict = async () => {
const response = await get('/auth/validateToken')
return response.data === true
}
/**
* 获取当前用户信息
* @returns {Promise<Object>} 用户信息
@@ -118,6 +123,7 @@ export default {
logout,
refreshToken,
validateToken,
validateTokenStrict,
getCurrentUserInfo,
checkPhone
}
+93 -99
View File
@@ -1,139 +1,117 @@
/**
* 爽文剧本服务
* 处理剧本的增删改查
*/
import { get, post, put, del } from './request.js'
export const getScriptList = async () => {
const response = await get('/epicScript/listAll')
return response
return get('/epicScript/listAll')
}
export const getScriptPage = async ({ pageNum = 1, pageSize = 10 }) => {
const response = await get('/epicScript/page', { pageNum, pageSize })
return response
return get('/epicScript/page', { pageNum, pageSize })
}
export const getScriptById = async (id) => {
const response = await get('/epicScript/detail', { id })
return response
return get('/epicScript/detail', { id })
}
export const getInspirationRecommendations = async () => {
return get('/epicScript/inspiration/recommendations')
}
export const getRandomInspirations = async (size = 3) => {
return get('/epicScript/inspiration/random', { size })
}
export const generateFromInspiration = async (payload) => {
return post('/epicScript/inspiration/generate', payload)
}
export const createScript = async (scriptData) => {
const requestData = transformToBackendFormat(scriptData)
const response = await post('/epicScript/create', requestData)
return response
return post('/epicScript/create', transformToBackendFormat(scriptData))
}
export const updateScript = async (scriptData) => {
const requestData = transformToBackendFormat(scriptData)
const response = await put('/epicScript/update', requestData)
return response
return put('/epicScript/update', transformToBackendFormat(scriptData))
}
export const selectScript = async (id) => {
const response = await put('/epicScript/select', { id })
return response
return put('/epicScript/select', { id })
}
export const deleteScript = async (id) => {
const response = await del('/epicScript/delete', { id })
return response
return del('/epicScript/delete', { id })
}
const textOrFallback = (value, fallback) => {
return value && String(value).trim() ? String(value).trim() : fallback
}
export const buildCharacterInfo = (profile = {}) => {
const hobbies = Array.isArray(profile.hobbies) && profile.hobbies.length
? profile.hobbies.join('、')
: '未设置'
const parts = [
`昵称:${textOrFallback(profile.nickname, '未设置')}`,
`性别:${textOrFallback(profile.gender, '未设置')}`,
`MBTI${textOrFallback(profile.mbti, '未设置')}`,
`星座:${textOrFallback(profile.zodiac, '未设置')}`,
`职业:${textOrFallback(profile.profession, '未设置')}`,
`兴趣:${hobbies}`
]
if (profile.future?.vision) parts.push(`未来愿景:${profile.future.vision}`)
if (profile.future?.ideal) parts.push(`理想生活:${profile.future.ideal}`)
return parts.join('\n')
}
export const buildLifeEventsSummary = (events = [], profile = {}) => {
const parts = []
const addMemory = (label, memory) => {
if (memory?.text) {
parts.push(`${label}${memory.date || '未知日期'}${memory.text}`)
}
}
addMemory('童年记忆', profile.childhood)
addMemory('高光时刻', profile.joy)
addMemory('低谷经历', profile.low)
events.forEach((event) => {
const tag = Array.isArray(event.tags) && event.tags.length ? ` #${event.tags.join(' #')}` : ''
parts.push(`【人生事件】${event.time || event.date || '未知日期'} ${event.title || '未命名'}${tag}${event.content || event.description || ''}`)
})
return parts.join('\n')
}
const transformToBackendFormat = (frontendData) => {
const {
id,
title,
theme,
style,
length,
content,
isSelected,
character,
events
events,
plotJson
} = frontendData
let title = theme || '我的剧本'
let plotIntro = ''
let plotTurning = ''
let plotClimax = ''
let plotEnding = ''
if (content) {
const sections = content.split(/【[^】]+】/)
const titles = content.match(/【[^】]+】/g) || []
titles.forEach((t, index) => {
const sectionContent = sections[index + 1]?.trim() || ''
if (t.includes('序幕') || t.includes('低谷')) {
plotIntro = sectionContent
} else if (t.includes('转折') || t.includes('契机')) {
plotTurning = sectionContent
} else if (t.includes('高潮') || t.includes('抉择')) {
plotClimax = sectionContent
} else if (t.includes('结局') || t.includes('开始')) {
plotEnding = sectionContent
}
})
}
let characterInfo = ''
if (character) {
const parts = [
`姓名:${character.nickname || '未设置'}`,
`性别:${character.gender || '未设置'}`,
`MBTI${character.mbti || '未设置'}`,
`星座:${character.zodiac || '未设置'}`,
`职业:${character.profession || '未设置'}`,
`兴趣爱好:${character.hobbies?.join(',') || '无'}`
]
if (character.future) {
if (character.future.vision) parts.push(`未来愿景:${character.future.vision}`)
if (character.future.ideal) parts.push(`理想生活:${character.future.ideal}`)
}
characterInfo = parts.join('\n')
}
let lifeEventsSummary = ''
const eventParts = []
if (character) {
if (character.childhood?.text) {
eventParts.push(`【童年记忆】(${character.childhood.date || '未知时间'}): ${character.childhood.text}`)
}
if (character.joy?.text) {
eventParts.push(`【高光时刻】(${character.joy.date || '未知时间'}): ${character.joy.text}`)
}
if (character.low?.text) {
eventParts.push(`【至暗时刻】(${character.low.date || '未知时间'}): ${character.low.text}`)
}
}
if (events && Array.isArray(events)) {
events.forEach(e => {
const dateStr = e.time || e.eventDate || '未知时间'
const titleStr = e.title || '无标题'
const contentStr = e.content || ''
eventParts.push(`【人生事件】(${dateStr}) ${titleStr}${contentStr ? ': ' + contentStr : ''}`)
})
}
lifeEventsSummary = eventParts.join('\n')
const scriptTitle = title || theme || '我的人生剧本'
const characterInfo = frontendData.characterInfo || buildCharacterInfo(character || {})
const lifeEventsSummary = frontendData.lifeEventsSummary || buildLifeEventsSummary(events || [], character || {})
return {
id,
title,
title: scriptTitle,
theme,
style,
length,
plotIntro,
plotTurning,
plotClimax,
plotEnding,
plotJson: content ? { fullContent: content } : null,
plotIntro: frontendData.plotIntro || '',
plotTurning: frontendData.plotTurning || '',
plotClimax: frontendData.plotClimax || '',
plotEnding: frontendData.plotEnding || '',
plotJson: plotJson || (content ? { fullContent: content } : null),
isSelected,
characterInfo,
lifeEventsSummary
@@ -156,7 +134,8 @@ export const transformToFrontendFormat = (backendData) => {
plotEnding,
plotJson,
isSelected,
createTime
createTime,
updateTime
} = backendData
let content = ''
@@ -171,6 +150,9 @@ export const transformToFrontendFormat = (backendData) => {
content = parts.join('\n\n')
}
const plainContent = content.replace(/[#>*_`-]/g, '').trim()
const summary = plainContent ? plainContent.slice(0, 90) : (theme || '一段正在生成的人生爽文。')
return {
id,
userId,
@@ -179,8 +161,15 @@ export const transformToFrontendFormat = (backendData) => {
style: style || '',
length: length || 'medium',
content,
isSelected: isSelected || false,
date: createTime ? new Date(createTime).toLocaleDateString() : new Date().toLocaleDateString()
summary,
plotJson,
isSelected: Boolean(isSelected),
createTime,
updateTime,
date: createTime ? String(createTime).slice(0, 10) : new Date().toLocaleDateString(),
mode: plotJson?.mode || 'custom',
prompt: plotJson?.prompt || '',
wordCount: content ? content.length : 0
}
}
@@ -193,10 +182,15 @@ export default {
getScriptList,
getScriptPage,
getScriptById,
getInspirationRecommendations,
getRandomInspirations,
generateFromInspiration,
createScript,
updateScript,
selectScript,
deleteScript,
buildCharacterInfo,
buildLifeEventsSummary,
transformToFrontendFormat,
transformListToFrontend
}
+86 -62
View File
@@ -1,11 +1,42 @@
import { getEnvValue, isDev } from '../config/env.js'
import { getEnvValue, getConfig } from '../config/env.js'
const API_BASE_URL = getEnvValue('API_BASE_URL')
/**
* 请求拦截处理
* 自动添加 token 到请求头
*/
const AUTH_PATH_PREFIX = '/auth/'
const isAuthPath = (path = '') => path.startsWith(AUTH_PATH_PREFIX)
const createRequestError = (message, meta = {}) => {
const error = new Error(message || 'Request failed')
Object.assign(error, meta)
return error
}
const clearAuthStorage = () => {
uni.removeStorageSync('access_token')
uni.removeStorageSync('refresh_token')
}
const logApi = (level, label, payload, force = false) => {
const debug = getEnvValue('DEBUG')
if (!debug && !force) return
const logger = level === 'error' ? console.error : console.log
logger(`[API] ${label}`, payload)
}
export const logRuntimeEnv = (source = 'startup') => {
const config = getConfig()
console.log('[ENV] runtime', {
source,
rawEnv: import.meta.env.VITE_APP_ENV,
mode: import.meta.env.MODE,
apiBaseUrl: config.API_BASE_URL,
wsUrl: config.WS_URL,
debug: config.DEBUG,
platform: typeof uni !== 'undefined' ? uni.getSystemInfoSync?.()?.platform : 'unknown'
})
}
const getHeaders = () => {
const token = uni.getStorageSync('access_token')
const headers = {
@@ -17,81 +48,82 @@ const getHeaders = () => {
return headers
}
/**
* 统一请求方法
* @param {Object} options - 请求配置
* @returns {Promise} 请求 Promise
*/
const request = (options) => {
const debug = getEnvValue('DEBUG')
const method = options.method || 'GET'
const fullUrl = `${API_BASE_URL}${options.url}`
const forceLog = isAuthPath(options.url)
// 请求日志
if (debug) {
console.log('[API Request]', {
method: options.method || 'GET',
url: fullUrl,
path: options.url,
env: import.meta.env.VITE_APP_ENV
})
}
logApi('log', 'request', {
method,
url: fullUrl,
path: options.url,
env: import.meta.env.VITE_APP_ENV || import.meta.env.MODE
}, forceLog)
return new Promise((resolve, reject) => {
uni.request({
url: fullUrl,
method: options.method || 'GET',
method,
data: options.data,
header: getHeaders(),
timeout: 30000,
success: (res) => {
const { data, statusCode } = res
const success = statusCode >= 200 && statusCode < 300 && (data?.code === 200 || data?.code === 0)
// 响应日志
if (debug) {
console.log('[API Response]', {
path: options.url,
status: statusCode,
code: data?.code,
success: data?.code === 200 || data?.code === 0
})
}
logApi('log', 'response', {
path: options.url,
status: statusCode,
code: data?.code,
success
}, forceLog || statusCode >= 400)
// 处理 401 未授权
if (statusCode === 401) {
uni.removeStorageSync('access_token')
uni.removeStorageSync('refresh_token')
uni.redirectTo({ url: '/pages/login/index' })
reject(new Error('登录已过期,请重新登录'))
if (!forceLog) {
clearAuthStorage()
uni.reLaunch({ url: '/pages/login/index' })
}
reject(createRequestError('Login expired, please login again', {
statusCode,
code: data?.code,
path: options.url,
isAuthRejected: true
}))
return
}
// 后端返回格式:{ code, message, data }
if (data.code === 200 || data.code === 0) {
if (success) {
resolve(data)
} else {
reject(new Error(data.message || '请求失败'))
return
}
reject(createRequestError(data?.message || 'Request failed', {
statusCode,
code: data?.code,
path: options.url,
response: data,
isAuthRejected: statusCode === 403 || data?.code === 401 || data?.code === 403
}))
},
fail: (err) => {
// 错误日志
if (debug) {
console.error('[API Error]', {
path: options.url,
error: err.errMsg,
env: import.meta.env.VITE_APP_ENV
})
}
reject(new Error(err.errMsg || '网络错误'))
logApi('error', 'fail', {
path: options.url,
url: fullUrl,
error: err.errMsg,
env: import.meta.env.VITE_APP_ENV || import.meta.env.MODE
}, true)
reject(createRequestError(err.errMsg || 'Network request failed', {
path: options.url,
isNetworkError: true,
originalError: err
}))
}
})
})
}
/**
* GET 请求
*/
export const get = (url, params = {}) => {
// 构建查询字符串
const queryString = Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&')
@@ -100,23 +132,14 @@ export const get = (url, params = {}) => {
return request({ url: fullUrl, method: 'GET' })
}
/**
* POST 请求
*/
export const post = (url, data = {}) => {
return request({ url, method: 'POST', data })
}
/**
* PUT 请求
*/
export const put = (url, data = {}) => {
return request({ url, method: 'PUT', data })
}
/**
* DELETE 请求
*/
export const del = (url, params = {}) => {
const queryString = Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
@@ -130,5 +153,6 @@ export default {
get,
post,
put,
del
del,
logRuntimeEnv
}
+186 -9
View File
@@ -13,6 +13,7 @@ const state = reactive({
userProfile: null,
events: [],
scripts: [],
inspirationRecommendations: [],
paths: [],
currentPath: null,
registrationData: {
@@ -29,10 +30,31 @@ const state = reactive({
}
})
const SESSION_STATUS = {
AUTHENTICATED: 'authenticated',
UNAUTHENTICATED: 'unauthenticated',
ERROR: 'error'
}
const logAuth = (label, payload = {}) => {
console.log(`[AUTH] ${label}`, payload)
}
const hasProfile = computed(() => {
return state.userProfile && state.userProfile.nickname
})
const resetAuthState = () => {
state.isLoggedIn = false
state.userInfo = null
state.userProfile = null
}
const clearStoredTokens = () => {
uni.removeStorageSync('access_token')
uni.removeStorageSync('refresh_token')
}
const login = async (phone, smsCode) => {
state.isLoading = true
try {
@@ -49,20 +71,33 @@ const login = async (phone, smsCode) => {
const logout = async () => {
await authService.logout()
state.isLoggedIn = false
state.userInfo = null
state.userProfile = null
resetAuthState()
}
const fetchUserProfile = async () => {
const fetchUserProfile = async (options = {}) => {
const { throwOnError = false } = options
try {
const res = await userProfileService.getCurrentProfile()
if (res.data) {
state.userProfile = userProfileService.transformToFrontendFormat(res.data)
Object.assign(state.registrationData, state.userProfile)
logAuth('profile:success', { hasProfile: true })
} else {
state.userProfile = null
logAuth('profile:empty', { hasProfile: false })
}
return res.data
} catch (error) {
state.userProfile = null
logAuth('profile:fail', {
message: error.message,
statusCode: error.statusCode,
code: error.code,
isNetworkError: Boolean(error.isNetworkError)
})
if (throwOnError) {
throw error
}
return null
}
}
@@ -116,6 +151,10 @@ const createEvent = async (eventData) => {
}
}
const getEventById = (id) => {
return state.events.find(event => String(event.id) === String(id)) || null
}
const fetchScripts = async () => {
try {
const res = await epicScriptService.getScriptList()
@@ -136,6 +175,48 @@ const createScript = async (scriptData) => {
}
}
const fetchInspirationRecommendations = async () => {
try {
const res = await epicScriptService.getInspirationRecommendations()
state.inspirationRecommendations = res.data || []
return state.inspirationRecommendations
} catch (error) {
state.inspirationRecommendations = []
return []
}
}
const fetchRandomInspirations = async (size = 3) => {
try {
const res = await epicScriptService.getRandomInspirations(size)
return res.data || []
} catch (error) {
return []
}
}
const generateScriptFromInspiration = async ({ prompt, style, length }) => {
try {
const profile = state.userProfile || state.registrationData
const res = await epicScriptService.generateFromInspiration({
prompt,
style,
length,
characterInfo: epicScriptService.buildCharacterInfo(profile),
lifeEventsSummary: epicScriptService.buildLifeEventsSummary(state.events, profile),
source: 'mini-program'
})
await fetchScripts()
return { success: true, data: res.data }
} catch (error) {
return { success: false, error: error.message }
}
}
const getScriptById = (id) => {
return state.scripts.find(script => String(script.id) === String(id)) || null
}
const selectScript = async (id) => {
try {
await epicScriptService.selectScript(id)
@@ -160,12 +241,101 @@ const setCurrentPath = (path) => {
state.currentPath = path
}
const initialize = async () => {
const restoreSession = async () => {
const token = uni.getStorageSync('access_token')
if (token) {
state.isLoggedIn = true
await fetchUserProfile()
const refreshToken = uni.getStorageSync('refresh_token')
logAuth('restore:start', {
hasAccessToken: Boolean(token),
hasRefreshToken: Boolean(refreshToken)
})
if (!token) {
clearStoredTokens()
resetAuthState()
logAuth('restore:unauthenticated', { reason: 'missing_access_token' })
return { status: SESSION_STATUS.UNAUTHENTICATED, reason: 'missing_access_token' }
}
try {
const isValid = await authService.validateTokenStrict()
if (!isValid) {
throw new Error('Token validation returned false')
}
logAuth('validate:success')
state.isLoggedIn = true
await fetchUserProfile({ throwOnError: true })
return {
status: SESSION_STATUS.AUTHENTICATED,
hasProfile: hasProfile.value,
reason: 'access_token_valid'
}
} catch (validateError) {
logAuth('validate:fail', {
message: validateError.message,
statusCode: validateError.statusCode,
code: validateError.code,
isNetworkError: Boolean(validateError.isNetworkError)
})
if (validateError.isNetworkError) {
resetAuthState()
return {
status: SESSION_STATUS.ERROR,
reason: 'validate_network_error',
error: validateError
}
}
}
if (!refreshToken) {
clearStoredTokens()
resetAuthState()
logAuth('refresh:skip', { reason: 'missing_refresh_token' })
return { status: SESSION_STATUS.UNAUTHENTICATED, reason: 'missing_refresh_token' }
}
try {
logAuth('refresh:start')
await authService.refreshToken()
logAuth('refresh:success')
state.isLoggedIn = true
await fetchUserProfile({ throwOnError: true })
return {
status: SESSION_STATUS.AUTHENTICATED,
hasProfile: hasProfile.value,
reason: 'refresh_token_valid'
}
} catch (refreshError) {
logAuth('refresh:fail', {
message: refreshError.message,
statusCode: refreshError.statusCode,
code: refreshError.code,
isNetworkError: Boolean(refreshError.isNetworkError)
})
if (refreshError.isNetworkError) {
resetAuthState()
return {
status: SESSION_STATUS.ERROR,
reason: 'refresh_network_error',
error: refreshError
}
}
clearStoredTokens()
resetAuthState()
return {
status: SESSION_STATUS.UNAUTHENTICATED,
reason: 'refresh_failed',
error: refreshError
}
}
}
const initialize = async () => {
return restoreSession()
}
export const useAppStore = () => {
@@ -180,12 +350,19 @@ export const useAppStore = () => {
setCurrentStep,
fetchEvents,
createEvent,
getEventById,
fetchScripts,
createScript,
fetchInspirationRecommendations,
fetchRandomInspirations,
generateScriptFromInspiration,
getScriptById,
selectScript,
fetchPaths,
setCurrentPath,
initialize
initialize,
restoreSession,
SESSION_STATUS
})
}
+6 -1
View File
@@ -21,7 +21,11 @@ const vueCompatPlugin = () => {
"export * from 'vue/dist/vue.runtime.esm-bundler.js'",
'export default VueRuntime',
'export const injectHook = () => {}',
'export const isInSSRComponentSetup = () => false'
'export const isInSSRComponentSetup = () => false',
'export const logError = (err) => { console.error(err) }',
'export const createVueApp = VueRuntime.createApp',
'export const onBeforeActivate = () => {}',
'export const onBeforeDeactivate = () => {}'
].join('\n')
}
}
@@ -38,6 +42,7 @@ export default defineConfig(({ command }) => {
envDir: __dirname,
publicDir: resolve(__dirname, 'static'),
server: {
port: 5175,
watch: {
usePolling: true,
interval: 100