feat: 2026-04-15~05-02 累积变更基线 — AI 重构 + Runtime Context + DWS 修复
涵盖(每条对应已存的审计记录): - AI 模块拆分:apps/backend/app/ai/apps -> prompts/(8 个 APP + app2a 派生) audit: 2026-04-20__ai-module-complete.md - admin-web AI 管理套件:AIDashboard / AIOperations / AIRunLogs / AITriggers / TriggerManager audit: 2026-04-21__admin-web-ai-management-suite.md - App2 财务洞察 prompt v3 -> v5.1 + 小程序 AI 接入(chat / board-finance) audit: 2026-04-22__app2_prompt_v5_1_and_miniprogram_ai_insight.md - App2 prewarm 全过滤器 + AI 触发器 cron reschedule audit: 2026-04-21__app2-finance-prewarm-all-filters.md migration: 20260420_ai_trigger_jobs_and_app2_prewarm.sql / 20260421_app2_prewarm_cron_reschedule.sql - AppType 联合类型对齐 + adminAiAppTypes.test.ts audit: 2026-04-30__admin_web_ai_app_type_alignment.md - DashScope tokens_used 提取修复 audit: 2026-04-30__backend_dashscope_tokens_used_extraction.md - App3 线索完整详情 prompt audit: 2026-05-01__backend_app3_full_detail_prompt.md - Runtime Context 沙箱(5-1~5-2 主线): - 后端 schema/service + admin_runtime_context / xcx_runtime_clock 两个 router - admin-web RuntimeContext.tsx + miniprogram runtime-clock.ts - migration: 20260501__runtime_context_sandbox.sql - tools/db/verify_admin_web_sandbox.py + verify_sandbox_end_to_end.py - database/changes: 7 份 sandbox_* 验证报告 - 飞球 DWS 修复:finance_area_daily 区域汇总 + task_engine 调整 + RLS 视图业务日上界(migration 20260502 + scripts/ops/gen_rls_business_date_migration.py) 合规: - .gitignore 启用 tmp/ 排除 - 不入仓:apps/etl/connectors/feiqiu/.env(API_TOKEN secret,本地修改保留) 待验证清单: - docs/audit/changes/2026-05-04__cumulative_baseline_pending_verification.md 每个主题的功能完整性 / 上线验证几乎都未收口,按优先级 P0~P3 逐一处理
This commit is contained in:
@@ -20,13 +20,47 @@ Component({
|
||||
type: Number,
|
||||
value: 200,
|
||||
},
|
||||
/**
|
||||
* Phase 2.3:来源页面标识(sourcePage),用于后端注入 page_context。
|
||||
* 取值参考 backend page_context.py 的 SUPPORTED_PAGE_TYPES:
|
||||
* board-finance / board-coach / board-customer / performance / task-list / my-profile 等。
|
||||
* 为空时不传入 chat 页。
|
||||
*/
|
||||
sourcePage: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/**
|
||||
* Phase 2.3:页面筛选参数(board-* 页面的 timeDimension/dimension/areaFilter 等),
|
||||
* 将作为 JSON 字符串附加到 url,在 chat 页面解析后随 SSE 请求发给后端。
|
||||
*/
|
||||
pageFilters: {
|
||||
type: Object,
|
||||
value: null as Record<string, string> | null,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onTap() {
|
||||
let url = this.data.targetUrl
|
||||
const params: string[] = []
|
||||
if (this.data.customerId) {
|
||||
url += `?customerId=${this.data.customerId}`
|
||||
params.push(`customerId=${encodeURIComponent(this.data.customerId)}`)
|
||||
}
|
||||
if (this.data.sourcePage) {
|
||||
params.push(`sourcePage=${encodeURIComponent(this.data.sourcePage)}`)
|
||||
}
|
||||
if (this.data.pageFilters && Object.keys(this.data.pageFilters).length > 0) {
|
||||
try {
|
||||
params.push(
|
||||
`pageFilters=${encodeURIComponent(JSON.stringify(this.data.pageFilters))}`,
|
||||
)
|
||||
} catch {
|
||||
// 非法 filters 忽略,不影响跳转
|
||||
}
|
||||
}
|
||||
if (params.length > 0) {
|
||||
url += (url.includes('?') ? '&' : '?') + params.join('&')
|
||||
}
|
||||
wx.navigateTo({
|
||||
url,
|
||||
|
||||
@@ -203,6 +203,8 @@
|
||||
<!-- AI 洞察 -->
|
||||
<!-- CHANGE 2026-03-12 | intent: H5 原型使用 SVG 机器人图标,不可用 emoji 替代;规范要求内联 SVG 导出为文件用 image 引用 -->
|
||||
<!-- CHANGE 2026-03-21 | P13 T6.1: AI 洞察改为动态渲染,移除硬编码文案 -->
|
||||
<!-- CHANGE 2026-04-22 | AI 洞察改版:两段式(dim标题 + 正文),第3条起省略为1行,加"查看全部"按钮 + 覆盖大弹窗 -->
|
||||
<!-- CHANGE 2026-04-22 seq11/12 置顶:AI 洞察区首屏为"本期总结"(健康度 + 跟踪),下方为明细 -->
|
||||
<view class="ai-insight-section">
|
||||
<view class="ai-insight-header">
|
||||
<view class="ai-insight-icon">
|
||||
@@ -210,11 +212,108 @@
|
||||
</view>
|
||||
<text class="ai-insight-title">AI 智能洞察</text>
|
||||
</view>
|
||||
<view class="ai-insight-body" wx:if="{{aiInsights.length > 0}}">
|
||||
<text class="ai-insight-line" wx:for="{{aiInsights}}" wx:key="index"><text class="ai-insight-dim">{{item.icon}} </text>{{item.text}}</text>
|
||||
|
||||
<!-- 本期总结卡片:seq11(健康度评级) + seq12(跟踪指标) -->
|
||||
<view class="ai-summary-card ai-summary-card--{{summaryLightType || 'neutral'}}" wx:if="{{aiInsightSummary.evaluation || aiInsightSummary.tracking}}">
|
||||
<view class="ai-summary-head">
|
||||
<view class="ai-summary-badge ai-summary-badge--{{summaryLightType || 'neutral'}}" wx:if="{{summaryLightLabel}}">
|
||||
<text>{{summaryLightLabel}}</text>
|
||||
</view>
|
||||
<text class="ai-summary-head-title">本期总结</text>
|
||||
</view>
|
||||
<!-- 2026-04-22 v4:evaluation title 与顶部徽章语义重复,隐藏 title 仅展示 body -->
|
||||
<view class="ai-summary-block" wx:if="{{aiInsightSummary.evaluation}}">
|
||||
<view class="ai-summary-block-body ai-summary-block-body-clamp">
|
||||
<text wx:for="{{aiInsightSummary.evaluation.bodySegs}}" wx:key="index" wx:for-item="seg" class="md-seg {{seg.bold ? 'md-bold' : ''}} {{seg.italic ? 'md-italic' : ''}}">{{seg.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ai-summary-divider" wx:if="{{aiInsightSummary.evaluation && aiInsightSummary.tracking}}"></view>
|
||||
<view class="ai-summary-block ai-summary-block--tracking" wx:if="{{aiInsightSummary.tracking}}">
|
||||
<text class="ai-summary-block-title">⏰ {{aiInsightSummary.tracking.title}}</text>
|
||||
<view class="ai-summary-block-body ai-summary-block-body-clamp">
|
||||
<text wx:for="{{aiInsightSummary.tracking.bodySegs}}" wx:key="index" wx:for-item="seg" class="md-seg {{seg.bold ? 'md-bold' : ''}} {{seg.italic ? 'md-italic' : ''}}">{{seg.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ai-insight-body" wx:else>
|
||||
<text class="ai-insight-line ai-insight-dim">暂无洞察数据</text>
|
||||
|
||||
<view class="ai-insight-body" wx:if="{{aiInsightDetails.length > 0}}">
|
||||
<view class="ai-insight-details-label" wx:if="{{aiInsightSummary.evaluation || aiInsightSummary.tracking}}">
|
||||
<text class="ai-insight-details-label-text">分板块明细洞察 · 仅展示前 3 条</text>
|
||||
</view>
|
||||
<!-- 2026-04-22 v3:seq 1/2/3 统一展示"标题 + 单行省略正文",详情看弹窗 -->
|
||||
<block wx:for="{{aiInsightDetails}}" wx:key="index" wx:if="{{index < 3}}">
|
||||
<view class="ai-insight-item">
|
||||
<view class="ai-insight-item-title">
|
||||
<text class="ai-insight-item-seq">{{index + 1}}</text>
|
||||
<text class="ai-insight-item-name">{{item.title}}</text>
|
||||
</view>
|
||||
<view class="ai-insight-item-body ai-insight-item-body-ellipsis">
|
||||
<text wx:for="{{item.bodySegs}}" wx:key="index" wx:for-item="seg" class="md-seg {{seg.bold ? 'md-bold' : ''}} {{seg.italic ? 'md-italic' : ''}}">{{seg.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<!-- 2026-04-22 v3:只要有洞察就显示"查看全部"按钮,引导进弹窗看完整内容 -->
|
||||
<view class="ai-insight-more" wx:if="{{aiInsightDetails.length > 0 || aiInsightSummary.evaluation}}" bindtap="openAllInsights" hover-class="ai-insight-more-hover">
|
||||
<text class="ai-insight-more-text">查看全部 AI 洞察 ›</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ai-insight-body" wx:elif="{{!aiInsightSummary.evaluation && !aiInsightSummary.tracking}}">
|
||||
<view class="ai-insight-item-body ai-insight-dim">暂无洞察数据</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI 洞察全部查看弹窗:覆盖除底部 tab 外整个页面;header / 可滚动 body / 底部通栏按钮 -->
|
||||
<view class="ai-modal-mask" wx:if="{{aiInsightsModalVisible}}" bindtap="closeAllInsights" catchtouchmove="_noop">
|
||||
<view class="ai-modal" catchtap="_noop">
|
||||
<view class="ai-modal-header">
|
||||
<view class="ai-modal-title-wrap">
|
||||
<view class="ai-insight-icon">
|
||||
<image src="/assets/icons/ai-robot.svg" mode="aspectFit" class="ai-insight-icon-img" />
|
||||
</view>
|
||||
<text class="ai-modal-title">AI 智能洞察 · 共 {{aiInsights.length}} 条</text>
|
||||
</view>
|
||||
<view class="ai-modal-close" bindtap="closeAllInsights" hover-class="ai-modal-close-hover">
|
||||
<text class="ai-modal-close-icon">✕</text>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view scroll-y="{{true}}" class="ai-modal-body" enhanced="{{true}}" show-scrollbar="{{false}}" bounces="{{true}}">
|
||||
<!-- 弹窗顶部:本期总结(seq11+seq12)- 同款 summary-card -->
|
||||
<view class="ai-summary-card ai-summary-card--{{summaryLightType || 'neutral'}} ai-summary-card--modal" wx:if="{{aiInsightSummary.evaluation || aiInsightSummary.tracking}}">
|
||||
<view class="ai-summary-head">
|
||||
<view class="ai-summary-badge ai-summary-badge--{{summaryLightType || 'neutral'}}" wx:if="{{summaryLightLabel}}">
|
||||
<text>{{summaryLightLabel}}</text>
|
||||
</view>
|
||||
<text class="ai-summary-head-title">本期总结</text>
|
||||
</view>
|
||||
<view class="ai-summary-block" wx:if="{{aiInsightSummary.evaluation}}">
|
||||
<view class="ai-summary-block-body">
|
||||
<text wx:for="{{aiInsightSummary.evaluation.bodySegs}}" wx:key="index" wx:for-item="seg" class="md-seg {{seg.bold ? 'md-bold' : ''}} {{seg.italic ? 'md-italic' : ''}}">{{seg.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ai-summary-divider" wx:if="{{aiInsightSummary.evaluation && aiInsightSummary.tracking}}"></view>
|
||||
<view class="ai-summary-block ai-summary-block--tracking" wx:if="{{aiInsightSummary.tracking}}">
|
||||
<text class="ai-summary-block-title">⏰ {{aiInsightSummary.tracking.title}}</text>
|
||||
<view class="ai-summary-block-body">
|
||||
<text wx:for="{{aiInsightSummary.tracking.bodySegs}}" wx:key="index" wx:for-item="seg" class="md-seg {{seg.bold ? 'md-bold' : ''}} {{seg.italic ? 'md-italic' : ''}}">{{seg.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="ai-modal-details-label" wx:if="{{(aiInsightSummary.evaluation || aiInsightSummary.tracking) && aiInsightDetails.length > 0}}">
|
||||
<text class="ai-modal-details-label-text">分板块明细洞察</text>
|
||||
</view>
|
||||
<view class="ai-modal-item" wx:for="{{aiInsightDetails}}" wx:key="index">
|
||||
<view class="ai-modal-item-title">
|
||||
<text class="ai-modal-item-seq">{{index + 1}}</text>
|
||||
<text class="ai-modal-item-name">{{item.title}}</text>
|
||||
</view>
|
||||
<view class="ai-modal-item-body">
|
||||
<text wx:for="{{item.bodySegs}}" wx:key="index" wx:for-item="seg" class="md-seg {{seg.bold ? 'md-bold' : ''}} {{seg.italic ? 'md-italic' : ''}}">{{seg.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ai-modal-footer-space"></view>
|
||||
</scroll-view>
|
||||
<view class="ai-modal-footer" bindtap="closeAllInsights" hover-class="ai-modal-footer-hover">关闭</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -492,7 +492,7 @@ AI_CHANGELOG
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
margin-bottom: 22rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
/* CHANGE 2026-03-12 | intent: H5 原型 AI 图标为 SVG 机器人(24×24 → 42rpx),不可用 emoji 替代 */
|
||||
@@ -545,6 +545,357 @@ AI_CHANGELOG
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
/* CHANGE 2026-04-22 v2 | AI 洞察列表项:与弹窗同款(序号徽章 + 标题 + 缩进正文) */
|
||||
.ai-insight-item {
|
||||
padding: 6rpx 0 10rpx 0;
|
||||
}
|
||||
|
||||
.ai-insight-item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.ai-insight-item-seq {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 8rpx;
|
||||
border-radius: 10rpx;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ai-insight-item-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ai-insight-item-body {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
line-height: 36rpx;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
text-indent: 48rpx;
|
||||
}
|
||||
|
||||
.ai-insight-item-body-ellipsis {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ===== 2026-04-22 seq11/12 置顶:本期总结(轻量版) ===== */
|
||||
/* 不用卡片容器,直接嵌入 AI 洞察区,用彩色小点 + 淡分隔区分层级 */
|
||||
.ai-summary-card {
|
||||
margin: 0 24rpx 18rpx 0;
|
||||
padding: 0 0 18rpx 0;
|
||||
border-bottom: 2rpx dashed rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ai-summary-card--modal {
|
||||
margin: 0 0 14rpx 0;
|
||||
}
|
||||
|
||||
/* 去掉左侧彩条,保留类名备用(无样式即不渲染) */
|
||||
.ai-summary-card--green,
|
||||
.ai-summary-card--yellow,
|
||||
.ai-summary-card--red,
|
||||
.ai-summary-card--neutral {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ai-summary-head {
|
||||
display: flex;
|
||||
align-items: baseline; /* 徽章与"本期总结"按文字基线对齐(字号不同时看起来贴底) */
|
||||
gap: 10rpx;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
|
||||
/* 徽章:去胶囊底,纯色粗字强调三色灯级别 */
|
||||
.ai-summary-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1rpx;
|
||||
padding: 0;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.ai-summary-badge--green { color: #4ade80; }
|
||||
.ai-summary-badge--yellow { color: #facc15; }
|
||||
.ai-summary-badge--red { color: #f87171; }
|
||||
.ai-summary-badge--neutral { color: rgba(255, 255, 255, 0.6); }
|
||||
|
||||
.ai-summary-head-title {
|
||||
font-size: 22rpx;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.ai-summary-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.ai-summary-block-title {
|
||||
font-size: 25rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
line-height: 36rpx;
|
||||
}
|
||||
|
||||
.ai-summary-block-body {
|
||||
font-size: 24rpx;
|
||||
line-height: 36rpx;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
/* 2026-04-22 v3:总结区 body 2 行省略,突出要点 */
|
||||
.ai-summary-block-body-clamp {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ai-summary-block--tracking {
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
.ai-summary-block--tracking .ai-summary-block-title {
|
||||
color: rgba(251, 191, 36, 0.85);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ai-summary-divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* "分板块明细洞察"分组标签 */
|
||||
.ai-insight-details-label {
|
||||
padding: 2rpx 0 12rpx 0;
|
||||
}
|
||||
|
||||
.ai-insight-details-label-text {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.42);
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.ai-modal-details-label {
|
||||
padding: 6rpx 0 8rpx 0;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.ai-modal-details-label-text {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.42);
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
/* 2026-04-22 小程序 Markdown 内联样式:**加粗** / *倾斜* */
|
||||
.md-seg {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.md-bold {
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.98);
|
||||
}
|
||||
|
||||
.md-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 加粗同时倾斜时组合生效(class 拼接即可) */
|
||||
|
||||
/* CHANGE 2026-04-22 v2 | "查看全部" 按钮居中 */
|
||||
.ai-insight-more {
|
||||
margin: 10rpx 24rpx 0 0;
|
||||
padding: 18rpx 24rpx;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
font-size: 26rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.ai-insight-more-hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.ai-insight-more-text {
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
/* CHANGE 2026-04-22 | AI 全部洞察弹窗:覆盖除底部 tab 外整个页面 */
|
||||
.ai-modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 110rpx; /* 避让自定义 tabBar 约 110rpx */
|
||||
bottom: calc(110rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
z-index: 9998;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 2026-04-22 v5:fixed top+bottom 固定高(scroll-view 在 flex max-height 里渲染溢出,放弃 auto 高度) */
|
||||
.ai-modal {
|
||||
position: fixed;
|
||||
left: 24rpx;
|
||||
right: 24rpx;
|
||||
top: 40rpx;
|
||||
bottom: calc(150rpx + env(safe-area-inset-bottom)); /* 110rpx tab + 40rpx 留白 */
|
||||
background: #2e2e2e;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 12rpx 48rpx rgba(0, 0, 0, 0.45);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.ai-modal-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 28rpx 20rpx 28rpx;
|
||||
border-bottom: 2rpx solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.ai-modal-title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ai-modal-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ai-modal-close {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.ai-modal-close-hover {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.ai-modal-close-icon {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
line-height: 28rpx;
|
||||
}
|
||||
|
||||
.ai-modal-body {
|
||||
/* 2026-04-22 v5:modal 固定高度后 flex:1 1 0 分配剩余空间给 scroll-view */
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
padding: 20rpx 28rpx 12rpx 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ai-modal-item {
|
||||
padding: 22rpx 0 22rpx 0;
|
||||
border-bottom: 2rpx dashed rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ai-modal-item:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ai-modal-item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.ai-modal-item-seq {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 36rpx;
|
||||
height: 36rpx;
|
||||
padding: 0 10rpx;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: #fff;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ai-modal-item-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ai-modal-item-body {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
line-height: 40rpx;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
text-indent: 48rpx; /* 首行缩进,和 seq 徽章对齐阅读感 */
|
||||
}
|
||||
|
||||
.ai-modal-footer-space {
|
||||
height: 24rpx;
|
||||
}
|
||||
|
||||
/* 2026-04-22 v3 | 弹窗底部整块作为关闭按钮:固定高度 100rpx,贯通整宽 */
|
||||
.ai-modal-footer {
|
||||
flex-shrink: 0;
|
||||
height: 100rpx;
|
||||
line-height: 100rpx;
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
letter-spacing: 8rpx;
|
||||
}
|
||||
|
||||
.ai-modal-footer-hover {
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
/* ===== 通用表格边框容器 ===== */
|
||||
.table-bordered {
|
||||
border: 2rpx solid #e7e7e7;
|
||||
|
||||
@@ -198,6 +198,12 @@ Page({
|
||||
/** 最后一次发送的用户消息内容(用于重试) */
|
||||
_lastUserContent: '',
|
||||
|
||||
/** SSE 断线重试次数 */
|
||||
_sseRetryCount: 0,
|
||||
|
||||
/** SSE 最大自动重试次数 */
|
||||
_SSE_MAX_RETRY: 2,
|
||||
|
||||
onShow() {
|
||||
// 权限守卫:检查登录状态、账号禁用、角色权限
|
||||
checkPageAccess('pages/chat/chat')
|
||||
@@ -227,10 +233,27 @@ Page({
|
||||
this.loadMessagesByContext('coach', options.coachId)
|
||||
} else if (options?.sourcePage) {
|
||||
// 看板类入口:保存来源页面和筛选参数
|
||||
const filterKeys = ['timeDimension', 'areaFilter', 'dimension', 'typeFilter', 'projectFilter']
|
||||
// Phase 2.3:优先解析 options.pageFilters(ai-float-button 传入的 JSON 字符串),
|
||||
// 回退到单独键(旧入口兼容:timeDimension / areaFilter 等)
|
||||
const pageFilters: Record<string, string> = {}
|
||||
for (const key of filterKeys) {
|
||||
if (options[key]) pageFilters[key] = options[key]
|
||||
if (options.pageFilters) {
|
||||
try {
|
||||
const parsed = JSON.parse(decodeURIComponent(options.pageFilters))
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
for (const k of Object.keys(parsed)) {
|
||||
const v = parsed[k]
|
||||
if (v != null) pageFilters[k] = String(v)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// JSON 解析失败忽略,回退到单键读取
|
||||
}
|
||||
}
|
||||
if (Object.keys(pageFilters).length === 0) {
|
||||
const filterKeys = ['timeDimension', 'areaFilter', 'dimension', 'typeFilter', 'projectFilter']
|
||||
for (const key of filterKeys) {
|
||||
if (options[key]) pageFilters[key] = options[key]
|
||||
}
|
||||
}
|
||||
this.setData({ sourcePage: options.sourcePage, pageFilters })
|
||||
this.loadMessagesByContext(options.sourcePage, '')
|
||||
@@ -418,6 +441,7 @@ Page({
|
||||
},
|
||||
// onDone: 流结束,更新消息 ID 和时间
|
||||
(messageId: number, createdAt: string) => {
|
||||
this._sseRetryCount = 0
|
||||
this.setData({
|
||||
[`messages[${aiIndex}].id`]: String(messageId),
|
||||
[`messages[${aiIndex}].timestamp`]: createdAt,
|
||||
@@ -477,8 +501,20 @@ Page({
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
// 网络错误或连接中断
|
||||
if (this.data.isStreaming) {
|
||||
// 网络错误或连接中断:无内容时指数退避重连
|
||||
this._sseTask = null
|
||||
if (!this.data.isStreaming) return
|
||||
if (fullContent === '' && this._sseRetryCount < this._SSE_MAX_RETRY) {
|
||||
this._sseRetryCount++
|
||||
const delay = (2 ** this._sseRetryCount) * 1000
|
||||
wx.showToast({ title: `重连中 ${this._sseRetryCount}/${this._SSE_MAX_RETRY}...`, icon: 'loading', duration: delay })
|
||||
this.setData({
|
||||
messages: this.data.messages.slice(0, aiIndex),
|
||||
isStreaming: false,
|
||||
streamingContent: '',
|
||||
})
|
||||
setTimeout(() => { this.triggerAIReply(chatId, content) }, delay)
|
||||
} else {
|
||||
const errorContent = fullContent || '连接中断,请重试'
|
||||
this.setData({
|
||||
[`messages[${aiIndex}].content`]: errorContent,
|
||||
@@ -487,7 +523,6 @@ Page({
|
||||
})
|
||||
wx.showToast({ title: '连接中断', icon: 'none', duration: 3000 })
|
||||
}
|
||||
this._sseTask = null
|
||||
},
|
||||
} as WechatMiniprogram.RequestOption)
|
||||
|
||||
@@ -509,4 +544,19 @@ Page({
|
||||
}, 50)
|
||||
}, 50)
|
||||
},
|
||||
|
||||
/** 点击引用卡片:跳转到对应详情页(Phase 2.1) */
|
||||
onRefCardTap(e: WechatMiniprogram.BaseEvent & { currentTarget: { dataset: { link?: string } } }) {
|
||||
const link = e.currentTarget?.dataset?.link
|
||||
if (!link || typeof link !== 'string') {
|
||||
return
|
||||
}
|
||||
wx.navigateTo({
|
||||
url: link,
|
||||
fail: (err) => {
|
||||
console.error('跳转引用详情失败', err)
|
||||
wx.showToast({ title: '跳转失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -92,13 +92,18 @@
|
||||
<text class="bubble-text">{{item.content}}</text>
|
||||
</view>
|
||||
<!-- AI 侧引用卡片(后端 referenceCard 附加在 assistant 回复中)-->
|
||||
<view class="inline-ref-card inline-ref-card--assistant" wx:if="{{item.referenceCard}}">
|
||||
<view
|
||||
class="inline-ref-card inline-ref-card--assistant {{item.referenceCard.link ? 'inline-ref-card--link' : ''}}"
|
||||
wx:if="{{item.referenceCard}}"
|
||||
data-link="{{item.referenceCard.link}}"
|
||||
bindtap="onRefCardTap"
|
||||
>
|
||||
<view class="inline-ref-header">
|
||||
<text class="inline-ref-type">{{item.referenceCard.type === 'customer' ? '👤 客户' : '📋 记录'}}</text>
|
||||
<text class="inline-ref-type">{{item.referenceCard.type === 'customer' ? '👤 客户' : item.referenceCard.type === 'assistant' ? '🧑\u200d🏫 助教' : item.referenceCard.type === 'task' ? '📋 任务' : '📋 记录'}}</text>
|
||||
<text class="inline-ref-title">{{fmt.safe(item.referenceCard.title)}}</text>
|
||||
</view>
|
||||
<text class="inline-ref-summary">{{fmt.safe(item.referenceCard.summary)}}</text>
|
||||
<view class="inline-ref-data">
|
||||
<view class="inline-ref-data" wx:if="{{item.referenceCard.dataList.length > 0}}">
|
||||
<view class="ref-data-item" wx:for="{{item.referenceCard.dataList}}" wx:for-item="entry" wx:key="key">
|
||||
<text class="ref-data-key">{{fmt.safe(entry.key)}}</text>
|
||||
<text class="ref-data-value">{{fmt.safe(entry.value)}}</text>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
| 2026-03-23 | 角色路由+页面权限守卫 | onShow 添加 checkPageAccess 权限守卫 |
|
||||
*/
|
||||
import { checkPageAccess } from '../../utils/auth-guard'
|
||||
import { fetchCustomerDetail } from '../../services/api'
|
||||
import { fetchCustomerDetail, fetchAICache } from '../../services/api'
|
||||
|
||||
interface ConsumptionRecord {
|
||||
id: string
|
||||
@@ -132,6 +132,7 @@ Page({
|
||||
}
|
||||
}
|
||||
this.setData({ pageState: 'normal' })
|
||||
if (id) this._loadAIInsight(id)
|
||||
} catch (e) {
|
||||
console.error('[customer-detail] loadDetail 失败:', e)
|
||||
this.setData({ pageState: 'error' })
|
||||
@@ -140,6 +141,23 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
async _loadAIInsight(memberId: string) {
|
||||
const cache = await fetchAICache('app7_customer_analysis', memberId)
|
||||
if (!cache?.result_json) return
|
||||
const rj = cache.result_json as any
|
||||
const COLORS = ['blue', 'indigo', 'purple', 'red', 'orange', 'yellow'] as const
|
||||
const strategies = Array.isArray(rj.strategies)
|
||||
? rj.strategies.map((s: any, i: number) => ({
|
||||
color: COLORS[i % COLORS.length],
|
||||
text: s.title || s.text || '',
|
||||
}))
|
||||
: []
|
||||
this.setData({
|
||||
'aiInsight.summary': rj.summary || '',
|
||||
'aiInsight.strategies': strategies,
|
||||
})
|
||||
},
|
||||
|
||||
onRetry() {
|
||||
const id = this.data.detail?.id || ''
|
||||
this.loadDetail(id)
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
*/
|
||||
import { checkPageAccess } from '../../utils/auth-guard'
|
||||
import { fetchCustomerConsumptionRecords } from '../../services/api'
|
||||
// CHANGE 2026-05-02 | 业务时钟:sandbox 模式下用 business_year/month 替代 new Date()
|
||||
import { getBusinessClock } from '../../utils/runtime-clock'
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -38,11 +40,12 @@ Page({
|
||||
monthLoading: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
async onLoad(options) {
|
||||
const id = options?.customerId || options?.id || ''
|
||||
const now = new Date()
|
||||
const currentYear = now.getFullYear()
|
||||
const currentMonth = now.getMonth() + 1
|
||||
// CHANGE 2026-05-02 | 默认当前年月走业务时钟,sandbox 模式按 sandbox_date 显示
|
||||
const clock = await getBusinessClock()
|
||||
const currentYear = clock.business_year
|
||||
const currentMonth = clock.business_month
|
||||
this.setData({
|
||||
customerId: id,
|
||||
currentYear,
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
| 2026-03-27 | 任务A 前端改造 | 修复数据转换(duration/income/timeRange/table/recordType),去掉 loadCustomerInfo 改从 records 响应取客户信息,新增 monthIncome 展示 |
|
||||
*/
|
||||
import { checkPageAccess } from '../../utils/auth-guard'
|
||||
// CHANGE 2026-05-02 | 业务时钟:sandbox 模式下用 business_year/month 替代 new Date()
|
||||
import { getBusinessClock } from '../../utils/runtime-clock'
|
||||
// CHANGE 2026-03-27 | 任务A A5: 去掉 fetchCustomerDetail,客户信息从 fetchCustomerRecords 响应中获取
|
||||
import { fetchCustomerRecords } from '../../services/api'
|
||||
import { formatCount } from '../../utils/money'
|
||||
@@ -86,12 +88,12 @@ Page({
|
||||
monthLoading: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
async onLoad(options) {
|
||||
const id = options?.customerId || options?.id || ''
|
||||
// 默认当前年月
|
||||
const now = new Date()
|
||||
const currentYear = now.getFullYear()
|
||||
const currentMonth = now.getMonth() + 1
|
||||
// CHANGE 2026-05-02 | 默认当前年月走业务时钟,sandbox 模式按 sandbox_date 显示
|
||||
const clock = await getBusinessClock()
|
||||
const currentYear = clock.business_year
|
||||
const currentMonth = clock.business_month
|
||||
this.setData({
|
||||
customerId: id,
|
||||
currentYear,
|
||||
|
||||
@@ -11,6 +11,8 @@ import { nameToAvatarColor } from '../../utils/avatar-color'
|
||||
import { formatMoney, formatCount } from '../../utils/money'
|
||||
import { formatHours } from '../../utils/time'
|
||||
import { API_BASE } from '../../utils/config'
|
||||
// CHANGE 2026-05-02 | 业务时钟:sandbox 模式下用 business_year/month 替代 new Date()
|
||||
import { getBusinessClock } from '../../utils/runtime-clock'
|
||||
|
||||
/** 中文课程类型 → 英文 CSS key(WXSS 不支持中文类名) */
|
||||
const COURSE_TAG_MAP: Record<string, string> = {
|
||||
@@ -58,7 +60,7 @@ Page({
|
||||
coachRole: '',
|
||||
storeName: '',
|
||||
|
||||
/** 月份切换 */
|
||||
/** 月份切换(onLoad 中改写为业务时钟当前年月) */
|
||||
currentYear: new Date().getFullYear(),
|
||||
currentMonth: new Date().getMonth() + 1,
|
||||
monthLabel: '',
|
||||
@@ -83,12 +85,13 @@ Page({
|
||||
hasMore: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const now = new Date()
|
||||
async onLoad() {
|
||||
// CHANGE 2026-05-02 | 用业务时钟初始化年月,sandbox 模式按 sandbox_date 显示
|
||||
const clock = await getBusinessClock()
|
||||
this.setData({
|
||||
currentYear: now.getFullYear(),
|
||||
currentMonth: now.getMonth() + 1,
|
||||
monthLabel: `${now.getFullYear()}年${now.getMonth() + 1}月`,
|
||||
currentYear: clock.business_year,
|
||||
currentMonth: clock.business_month,
|
||||
monthLabel: `${clock.business_year}年${clock.business_month}月`,
|
||||
})
|
||||
this.loadBanner()
|
||||
this.loadData()
|
||||
@@ -140,11 +143,13 @@ Page({
|
||||
wx.showLoading({ title: '加载中...', mask: true })
|
||||
|
||||
// 预估规则:当月且当前日期 ≤ 5号(全小程序统一)
|
||||
const now = new Date()
|
||||
// CHANGE 2026-05-02 | 用业务时钟,sandbox 模式按 sandbox_date 判断
|
||||
const clock = await getBusinessClock()
|
||||
const { currentYear, currentMonth } = this.data
|
||||
const isCurrentMonth = currentYear === now.getFullYear()
|
||||
&& currentMonth === now.getMonth() + 1
|
||||
&& now.getDate() <= 5
|
||||
const businessDay = parseInt(clock.business_date.slice(8, 10), 10) || 1
|
||||
const isCurrentMonth = currentYear === clock.business_year
|
||||
&& currentMonth === clock.business_month
|
||||
&& businessDay <= 5
|
||||
|
||||
try {
|
||||
const res = await fetchPerformanceRecords({
|
||||
@@ -243,7 +248,7 @@ Page({
|
||||
},
|
||||
|
||||
/** 切换月份 */
|
||||
switchMonth(e: WechatMiniprogram.TouchEvent) {
|
||||
async switchMonth(e: WechatMiniprogram.TouchEvent) {
|
||||
const direction = e.currentTarget.dataset.direction as 'prev' | 'next'
|
||||
let { currentYear, currentMonth } = this.data
|
||||
|
||||
@@ -255,11 +260,13 @@ Page({
|
||||
if (currentMonth > 12) { currentMonth = 1; currentYear++ }
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const nowYear = now.getFullYear()
|
||||
const nowMonth = now.getMonth() + 1
|
||||
// CHANGE 2026-05-02 | 用业务时钟,sandbox 模式下不允许"翻到 sandbox_date 之后"
|
||||
const clock = await getBusinessClock()
|
||||
const nowYear = clock.business_year
|
||||
const nowMonth = clock.business_month
|
||||
const businessDay = parseInt(clock.business_date.slice(8, 10), 10) || 1
|
||||
const canGoNext = currentYear < nowYear || (currentYear === nowYear && currentMonth < nowMonth)
|
||||
const isCurrentMonth = currentYear === nowYear && currentMonth === nowMonth && now.getDate() <= 5
|
||||
const isCurrentMonth = currentYear === nowYear && currentMonth === nowMonth && businessDay <= 5
|
||||
|
||||
// 月份切换重置分页到第 1 页
|
||||
this.setData({
|
||||
|
||||
@@ -10,6 +10,8 @@ import { fetchMe, fetchPerformanceOverview } from '../../services/api'
|
||||
import { nameToAvatarColor } from '../../utils/avatar-color'
|
||||
// CHANGE 2026-03-27 | 头像:需要 API_BASE 构建头像完整 URL
|
||||
import { API_BASE } from '../../utils/config'
|
||||
// CHANGE 2026-05-02 | 业务时钟:sandbox 模式下用 business_year/month 替代 new Date()
|
||||
import { getBusinessClock } from '../../utils/runtime-clock'
|
||||
|
||||
/** 中文课程类型 → 英文 CSS key(WXSS 不支持中文类名) */
|
||||
const COURSE_TAG_MAP: Record<string, string> = {
|
||||
@@ -118,15 +120,16 @@ Page({
|
||||
this.setData({ pageState: 'loading' })
|
||||
wx.showLoading({ title: '加载中...', mask: true })
|
||||
|
||||
// G2:当月预估判断
|
||||
const now = new Date()
|
||||
const nowYear = now.getFullYear()
|
||||
const nowMonth = now.getMonth() + 1
|
||||
// CHANGE 2026-05-02 | G2 当月预估判断改用业务时钟(sandbox 模式按 sandbox_date 判断)
|
||||
const clock = await getBusinessClock()
|
||||
const nowYear = clock.business_year
|
||||
const nowMonth = clock.business_month
|
||||
const businessDay = parseInt(clock.business_date.slice(8, 10), 10) || 1
|
||||
// TODO: 联调时从接口参数或页面参数获取 year/month
|
||||
const year = nowYear
|
||||
const month = nowMonth
|
||||
// CHANGE 2026-03-24 | 预估规则:当月且当前日期 ≤ 5号才显示"预估"
|
||||
const isCurrentMonth = year === nowYear && month === nowMonth && now.getDate() <= 5
|
||||
const isCurrentMonth = year === nowYear && month === nowMonth && businessDay <= 5
|
||||
|
||||
try {
|
||||
// 并行请求用户信息和绩效概览
|
||||
|
||||
@@ -20,6 +20,8 @@ import { fetchTasks, fetchMe, pinTask, unpinTask, abandonTask, restoreTask, crea
|
||||
import { formatMoney } from '../../utils/money'
|
||||
import { formatDeadline } from '../../utils/time'
|
||||
import { formatStorageLevel } from '../../utils/storage-level'
|
||||
// CHANGE 2026-05-02 | 业务时钟:sandbox 模式下用 business_year/month 替代 new Date()
|
||||
import { getBusinessClock } from '../../utils/runtime-clock'
|
||||
// CHANGE 2026-03-27 | 头像:需要 API_BASE 构建头像完整 URL
|
||||
import { API_BASE } from '../../utils/config'
|
||||
import {
|
||||
@@ -386,9 +388,11 @@ Page({
|
||||
}
|
||||
|
||||
// G2: 当月预估判断
|
||||
const now = new Date()
|
||||
const nowYear = now.getFullYear()
|
||||
const nowMonth = now.getMonth() + 1
|
||||
// CHANGE 2026-05-02 | 用业务时钟,sandbox 模式按 sandbox_date 判断
|
||||
const clock = await getBusinessClock()
|
||||
const nowYear = clock.business_year
|
||||
const nowMonth = clock.business_month
|
||||
const businessDay = parseInt(clock.business_date.slice(8, 10), 10) || 1
|
||||
const incomeMonth = perfData.incomeMonth
|
||||
let dataYear = nowYear
|
||||
let dataMonth = nowMonth
|
||||
@@ -397,7 +401,7 @@ Page({
|
||||
if (parts) dataMonth = parseInt(parts[1], 10)
|
||||
}
|
||||
// CHANGE 2026-03-24 | 预估规则:当月且当前日期 ≤ 5号才显示"预估"(全小程序统一)
|
||||
const isCurrentMonth = dataYear === nowYear && dataMonth === nowMonth && now.getDate() <= 5
|
||||
const isCurrentMonth = dataYear === nowYear && dataMonth === nowMonth && businessDay <= 5
|
||||
|
||||
this.setData({
|
||||
pageState: totalCount > 0 ? 'normal' : 'empty',
|
||||
|
||||
@@ -35,6 +35,31 @@ export async function fetchMe(): Promise<ApiUserInfo> {
|
||||
return request({ url: '/api/xcx/me', method: 'GET', needAuth: true })
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 业务时钟(沙箱支持)
|
||||
// ============================================
|
||||
|
||||
export interface RuntimeClock {
|
||||
mode: 'live' | 'sandbox'
|
||||
business_date: string // YYYY-MM-DD
|
||||
business_year: number
|
||||
business_month: number
|
||||
business_year_month: string // YYYY-MM
|
||||
business_now: string
|
||||
is_sandbox: boolean
|
||||
sandbox_date: string | null
|
||||
sandbox_instance_id: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前门店的业务时钟(live 真实日,sandbox 模拟日)。
|
||||
* 沙箱模式下,小程序所有依赖"当前年月"的请求都应使用此结果,
|
||||
* 避免直接 ``new Date()`` 导致与后端 sandbox_date 不一致。
|
||||
*/
|
||||
export async function fetchRuntimeClock(): Promise<RuntimeClock> {
|
||||
return request({ url: '/api/xcx/runtime/clock', method: 'GET', needAuth: true })
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 任务模块
|
||||
// ============================================
|
||||
@@ -413,6 +438,26 @@ export async function sendChatMessage(chatId: string, content: string): Promise<
|
||||
// 配置模块
|
||||
// ============================================
|
||||
|
||||
/** AI 缓存查询(Phase 2.5) */
|
||||
export async function fetchAICache(cacheType: string, targetId: string): Promise<{
|
||||
result_json: Record<string, any> | null;
|
||||
score: number | null;
|
||||
} | null> {
|
||||
try {
|
||||
const data = await request({
|
||||
url: `/api/ai/cache/${cacheType}`,
|
||||
method: 'GET',
|
||||
data: { target_id: targetId },
|
||||
needAuth: true,
|
||||
})
|
||||
if (!data) return null
|
||||
const d = data as any
|
||||
return { result_json: d.result_json ?? null, score: d.score ?? null }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** 项目类型筛选器列表(CONFIG-1) */
|
||||
// CHANGE 2026-03-20 | R3 修复:value 改为数据库 category_code,fallback 与后端一致
|
||||
export async function fetchSkillTypes(): Promise<Array<{ value: string; text: string; icon?: string }>> {
|
||||
|
||||
83
apps/miniprogram/miniprogram/utils/runtime-clock.ts
Normal file
83
apps/miniprogram/miniprogram/utils/runtime-clock.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// 业务时钟缓存
|
||||
//
|
||||
// sandbox 模式下,小程序的 performance / task-list / customer-records 等页面
|
||||
// 需要按"业务日"而不是"真实今天"构造请求参数。
|
||||
//
|
||||
// 用法:
|
||||
// import { getBusinessClock, getBusinessYearMonth } from '../../utils/runtime-clock'
|
||||
// const clock = await getBusinessClock()
|
||||
// wx.request({ url: ..., data: { year: clock.business_year, month: clock.business_month } })
|
||||
//
|
||||
// 缓存策略:
|
||||
// - 单例 in-memory cache,最多 60 秒;过期后自动重新拉取。
|
||||
// - 切换沙箱后,建议页面调用 `clearBusinessClockCache()` 主动失效。
|
||||
|
||||
import { fetchRuntimeClock, type RuntimeClock } from '../services/api'
|
||||
|
||||
const TTL_MS = 60_000 // 60 秒缓存,足以覆盖一次页面进入
|
||||
|
||||
let cached: { value: RuntimeClock; ts: number } | null = null
|
||||
let inflight: Promise<RuntimeClock> | null = null
|
||||
|
||||
/** 主动清空业务时钟缓存(沙箱切换、登出后调用)。 */
|
||||
export function clearBusinessClockCache(): void {
|
||||
cached = null
|
||||
inflight = null
|
||||
}
|
||||
|
||||
/** 拉取业务时钟(可能命中缓存)。失败时降级为本地"今天"。 */
|
||||
export async function getBusinessClock(force = false): Promise<RuntimeClock> {
|
||||
const now = Date.now()
|
||||
if (!force && cached && now - cached.ts < TTL_MS) {
|
||||
return cached.value
|
||||
}
|
||||
if (inflight) {
|
||||
return inflight
|
||||
}
|
||||
inflight = (async () => {
|
||||
try {
|
||||
const clock = await fetchRuntimeClock()
|
||||
cached = { value: clock, ts: Date.now() }
|
||||
return clock
|
||||
} catch (err) {
|
||||
console.warn('[runtime-clock] 拉取业务时钟失败,降级为本地时间', err)
|
||||
return localFallback()
|
||||
} finally {
|
||||
inflight = null
|
||||
}
|
||||
})()
|
||||
return inflight
|
||||
}
|
||||
|
||||
/** 便捷方法:返回业务年月 (YYYY-MM)。 */
|
||||
export async function getBusinessYearMonth(): Promise<{ year: number; month: number; label: string }> {
|
||||
const clock = await getBusinessClock()
|
||||
return {
|
||||
year: clock.business_year,
|
||||
month: clock.business_month,
|
||||
label: `${clock.business_year}年${clock.business_month}月`,
|
||||
}
|
||||
}
|
||||
|
||||
/** 便捷方法:返回业务日 (YYYY-MM-DD)。 */
|
||||
export async function getBusinessDate(): Promise<string> {
|
||||
return (await getBusinessClock()).business_date
|
||||
}
|
||||
|
||||
function localFallback(): RuntimeClock {
|
||||
const d = new Date()
|
||||
const year = d.getFullYear()
|
||||
const month = d.getMonth() + 1
|
||||
const ymd = `${year}-${String(month).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||
return {
|
||||
mode: 'live',
|
||||
business_date: ymd,
|
||||
business_year: year,
|
||||
business_month: month,
|
||||
business_year_month: `${year}-${String(month).padStart(2, '0')}`,
|
||||
business_now: d.toISOString(),
|
||||
is_sandbox: false,
|
||||
sandbox_date: null,
|
||||
sandbox_instance_id: null,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user