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:
@@ -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}
|
||||
@@ -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获取爽文剧本详情
|
||||
*/
|
||||
|
||||
+28
@@ -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;
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
+17
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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})")
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+473
-25
@@ -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",
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user