Show app logos in social BI
This commit is contained in:
parent
85e2e76878
commit
54b584c096
@ -4022,7 +4022,7 @@
|
||||
"x-permission": "overview:view",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/StatisticsObjectResponse"
|
||||
"$ref": "#/components/responses/SocialBiMasterResponse"
|
||||
}
|
||||
},
|
||||
"x-permissions": ["overview:view"]
|
||||
@ -7978,6 +7978,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SocialBiMasterResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseSocialBiMaster"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SocialBiRequirementsResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
@ -9626,6 +9636,61 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseSocialBiMaster": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Envelope"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/SocialBiMasterResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"SocialBiMasterResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SocialBiMasterApp"
|
||||
}
|
||||
},
|
||||
"regions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"operators": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"access": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": ["apps", "regions", "operators", "access"]
|
||||
},
|
||||
"SocialBiMasterApp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app_code": { "type": "string" },
|
||||
"app_name": { "type": "string" },
|
||||
"logo_url": { "type": "string" },
|
||||
"kind": { "type": "string" }
|
||||
},
|
||||
"required": ["app_code", "app_name", "logo_url", "kind"]
|
||||
},
|
||||
"ApiResponseSocialBiRequirements": {
|
||||
"allOf": [
|
||||
{
|
||||
|
||||
@ -263,8 +263,8 @@ test("routes databi social page to Social BI and loads real overview rows", asyn
|
||||
fetchSocialBiMaster.mockResolvedValue({
|
||||
access: { all: true, scopes: [] },
|
||||
apps: [
|
||||
{ app_code: "lalu", app_name: "Lalu", kind: "hyapp" },
|
||||
{ app_code: "aslan", app_name: "Aslan", kind: "legacy" }
|
||||
{ app_code: "lalu", app_name: "Lalu", kind: "hyapp", logo_url: "https://media.example.com/lalu.png" },
|
||||
{ app_code: "aslan", app_name: "Aslan", kind: "legacy", logo_url: "https://media.example.com/aslan.png" }
|
||||
],
|
||||
operators: [
|
||||
{ account: "omar", name: "Omar", scopes: [{ app_code: "aslan", region_id: 11 }], team: "中东运营部", team_id: 2, user_id: 9 }
|
||||
@ -340,6 +340,8 @@ test("routes databi social page to Social BI and loads real overview rows", asyn
|
||||
// 经营概览:App 名、Hero 汇总充值(12,300 + 5,000 USD 分 = $173)、
|
||||
// App 对比里 Lalu 的单 App 充值($123)、区域 Top5 里的中东大区。
|
||||
expect(screen.getAllByText("Lalu").length).toBeGreaterThan(0);
|
||||
expect(document.querySelector('img[src="https://media.example.com/lalu.png"]')).toBeTruthy();
|
||||
expect(document.querySelector('img[src="https://media.example.com/aslan.png"]')).toBeTruthy();
|
||||
expect(screen.getAllByText("充值 ($)").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("付费用户")).toBeTruthy();
|
||||
expect(screen.getAllByText("$173").length).toBeGreaterThan(0);
|
||||
|
||||
@ -20,7 +20,7 @@ import {
|
||||
writeStateToURL
|
||||
} from "./state.js";
|
||||
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
|
||||
import { appColor } from "./metrics.js";
|
||||
import { AppIdentity } from "@/shared/ui/AppIdentity.jsx";
|
||||
import { SOCIAL_BI_FUNNEL_APP_CODES, useSocialBiData } from "./useSocialBiData.js";
|
||||
import { OverviewView } from "./views/OverviewView.jsx";
|
||||
import { RegionsView } from "./views/RegionsView.jsx";
|
||||
@ -49,6 +49,14 @@ export function useSocialBi() {
|
||||
return context;
|
||||
}
|
||||
|
||||
// Social BI 的统计结果只携带 app_code/app_name;Logo 从 master 目录按 app_code 回填,保证所有视图使用同一份品牌事实。
|
||||
export function SocialAppIdentity({ app, appCode, appName, className = "", size = "small" }) {
|
||||
const context = useSocialBi();
|
||||
const resolvedCode = String(app?.app_code || app?.appCode || appCode || "").trim().toLowerCase();
|
||||
const catalogApp = (context.master?.apps || []).find((item) => String(item.app_code || item.appCode || "").trim().toLowerCase() === resolvedCode);
|
||||
return <AppIdentity app={catalogApp || app} appCode={resolvedCode} appName={appName} className={className} size={size} />;
|
||||
}
|
||||
|
||||
export function SocialBiApp() {
|
||||
const initial = useMemo(() => readStateFromURL(), []);
|
||||
const [filters, setFilters] = useState(initial.filters);
|
||||
@ -261,8 +269,7 @@ function AppChips({ data, filters, updateFilter, viewKey }) {
|
||||
onClick={() => updateFilter("apps", toggleMultiValue(filters.apps, app.app_code, allCodes))}
|
||||
type="button"
|
||||
>
|
||||
<span className="sbi-chip-dot" style={{ background: appColor(app.app_code, allCodes) }} />
|
||||
{app.app_name || app.app_code}
|
||||
<AppIdentity app={app} size="small" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@ -316,7 +323,8 @@ function RegionSelect({ data, filters, updateFilter }) {
|
||||
onClick={() => updateFilter("regions", toggleMultiValue(filters.regions, `app:${group.app.app_code}`))}
|
||||
type="button"
|
||||
>
|
||||
{group.app.app_name || group.app.app_code} · 全部区域
|
||||
<AppIdentity app={group.app} size="small" />
|
||||
<span>全部区域</span>
|
||||
</button>
|
||||
{group.regions.map((region) => {
|
||||
const value = `${region.app_code}:${region.region_id}`;
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Button from "@mui/material/Button";
|
||||
import FileDownloadOutlined from "@mui/icons-material/FileDownloadOutlined";
|
||||
import { useSocialBi } from "../SocialBiApp.jsx";
|
||||
import { SocialAppIdentity, useSocialBi } from "../SocialBiApp.jsx";
|
||||
import {
|
||||
METRIC_GROUPS,
|
||||
aggregateMetricMaps,
|
||||
@ -25,10 +25,10 @@ const EMPTY_ROWS = [];
|
||||
|
||||
// 维度列定义:value 取行上的展示文本,totalText 是合计行占位(日期列"汇总",其余"全部")。
|
||||
const DIM_COLUMNS = {
|
||||
app: { label: "App", totalText: "全部", value: (row) => row.app_name || row.app_code || "--" },
|
||||
country: { label: "国家", totalText: "全部", value: (row) => row.country_name || row.country_code || "--" },
|
||||
day: { label: "日期", totalText: "汇总", value: (row) => row.stat_day || "--" },
|
||||
region: { label: "区域", totalText: "全部", value: (row) => row.region_name || row.region_code || "--" }
|
||||
app: { key: "app", label: "App", totalText: "全部", value: (row) => row.app_name || row.app_code || "--" },
|
||||
country: { key: "country", label: "国家", totalText: "全部", value: (row) => row.country_name || row.country_code || "--" },
|
||||
day: { key: "day", label: "日期", totalText: "汇总", value: (row) => row.stat_day || "--" },
|
||||
region: { key: "region", label: "区域", totalText: "全部", value: (row) => row.region_name || row.region_code || "--" }
|
||||
};
|
||||
|
||||
// 五种明细维度:key 与 derived 的行集字段一一对应。
|
||||
@ -702,7 +702,9 @@ export function DataTableView() {
|
||||
<tr key={rowIdentity(row, dim.key, rowIndex)}>
|
||||
{dimColumns.map((column, index) => (
|
||||
<td className={index === 0 ? "is-left sbi-dt-sticky" : "is-left"} key={column.label}>
|
||||
{column.value(row)}
|
||||
{column.key === "app" ? (
|
||||
<SocialAppIdentity appCode={row.app_code} appName={row.app_name} />
|
||||
) : column.value(row)}
|
||||
</td>
|
||||
))}
|
||||
{visibleMetrics.map((key) => (
|
||||
@ -784,7 +786,9 @@ export function DataTableView() {
|
||||
{requirementRows.map((row, rowIndex) => (
|
||||
<tr key={requirementRowIdentity(row, activeRequirementSection.key, rowIndex)}>
|
||||
<td className="is-left sbi-dt-sticky">{row.stat_day || "--"}</td>
|
||||
<td className="is-left">{requirementAppName(row)}</td>
|
||||
<td className="is-left">
|
||||
<SocialAppIdentity appCode={row.app_code} appName={requirementAppName(row)} />
|
||||
</td>
|
||||
{requirementColumns.map((column) => (
|
||||
<td key={column.key}>{formatRequirementValue(column, row[column.key])}</td>
|
||||
))}
|
||||
@ -823,7 +827,9 @@ export function DataTableView() {
|
||||
>
|
||||
<td className="is-left sbi-dt-sticky">{requirementGameLabel(row)}</td>
|
||||
<td className="is-left">{row.stat_day || "--"}</td>
|
||||
<td className="is-left">{requirementAppName(row)}</td>
|
||||
<td className="is-left">
|
||||
<SocialAppIdentity appCode={row.app_code} appName={requirementAppName(row)} />
|
||||
</td>
|
||||
{requirementGameData.columns.map((column) => (
|
||||
<td key={column.key}>{formatRequirementValue(column, row[column.key])}</td>
|
||||
))}
|
||||
|
||||
@ -5,7 +5,7 @@ import { useMemo, useState } from "react";
|
||||
import { EChart } from "../../charts/EChart.jsx";
|
||||
import { formatCount, formatRatioPercent, isBlank } from "../format.js";
|
||||
import { rangeLabel } from "../state.js";
|
||||
import { useSocialBi } from "../SocialBiApp.jsx";
|
||||
import { SocialAppIdentity, useSocialBi } from "../SocialBiApp.jsx";
|
||||
import "./funnel-view.css";
|
||||
|
||||
const COHORT_DIMENSIONS = [
|
||||
@ -87,7 +87,7 @@ export function FunnelView() {
|
||||
</div>
|
||||
<div className="sbi-funnel-apps">
|
||||
{appRows.map((app) => (
|
||||
<span key={app.app_code}>{app.app_name || app.app_code}</span>
|
||||
<SocialAppIdentity appCode={app.app_code} appName={app.app_name} key={app.app_code} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -5,7 +5,7 @@ import { useMemo, useState } from "react";
|
||||
import { EChart } from "../../charts/EChart.jsx";
|
||||
import { deltaDirection, formatCount, formatDeltaRate, formatMoney, isBlank } from "../format.js";
|
||||
import { aggregateMetricMaps, appColor, formatMetric, metricChartValue, metricLabel, metricTooltip } from "../metrics.js";
|
||||
import { useSocialBi } from "../SocialBiApp.jsx";
|
||||
import { SocialAppIdentity, useSocialBi } from "../SocialBiApp.jsx";
|
||||
import { bucketDay } from "../state.js";
|
||||
import "./overview-view.css";
|
||||
|
||||
@ -411,8 +411,7 @@ export function OverviewView() {
|
||||
return (
|
||||
<div className="sbi-ov-compare-row" key={row.app_code}>
|
||||
<span className="sbi-ov-compare-name">
|
||||
<span className="sbi-ov-dot" style={{ background: color }} />
|
||||
{row.app_name}
|
||||
<SocialAppIdentity appCode={row.app_code} appName={row.app_name} />
|
||||
</span>
|
||||
<div className="sbi-ov-bar-track">
|
||||
<div className="sbi-ov-bar-fill" style={{ background: color, width: `${share}%` }} />
|
||||
@ -464,8 +463,7 @@ export function OverviewView() {
|
||||
<span className="sbi-ov-top-name">
|
||||
<strong>{row.region_name}</strong>
|
||||
<span className="sbi-badge">
|
||||
<span className="sbi-ov-dot" style={{ background: color }} />
|
||||
{row.app_name}
|
||||
<SocialAppIdentity appCode={row.app_code} appName={row.app_name} />
|
||||
</span>
|
||||
</span>
|
||||
<div className="sbi-ov-bar-track">
|
||||
|
||||
@ -8,8 +8,8 @@ import { EChart } from "../../charts/EChart.jsx";
|
||||
import { resolveCountryMeta } from "../../data/countryMeta.js";
|
||||
import worldMap from "../../data/world.json";
|
||||
import { formatCount, formatMoneyMinor, formatRatioPercent, isBlank } from "../format.js";
|
||||
import { aggregateMetricMaps, appColor, formatMetric, metricChartValue, metricLabel, metricTooltip } from "../metrics.js";
|
||||
import { useSocialBi } from "../SocialBiApp.jsx";
|
||||
import { aggregateMetricMaps, formatMetric, metricChartValue, metricLabel, metricTooltip } from "../metrics.js";
|
||||
import { SocialAppIdentity, useSocialBi } from "../SocialBiApp.jsx";
|
||||
import "./regions-view.css";
|
||||
|
||||
// EChart.jsx 未注册 visualMap 组件,这里按需补注册(echarts.use 幂等,不影响其它图表)。
|
||||
@ -69,7 +69,7 @@ function byRechargeDesc(a, b) {
|
||||
}
|
||||
|
||||
export function RegionsView() {
|
||||
const { appCodes, derived, isLoading } = useSocialBi();
|
||||
const { derived, isLoading } = useSocialBi();
|
||||
const { appTotals, countryTotals, regionTotals } = derived;
|
||||
const [mapMetric, setMapMetric] = useState("recharge_usd_minor");
|
||||
const [expandedRegions, setExpandedRegions] = useState(() => new Set());
|
||||
@ -307,8 +307,7 @@ export function RegionsView() {
|
||||
</span>
|
||||
) : null}
|
||||
<span className="sbi-rg-app-tag">
|
||||
<span className="sbi-rg-app-dot" style={{ background: appColor(row.app_code, appCodes) }} />
|
||||
{row.app_name}
|
||||
<SocialAppIdentity appCode={row.app_code} appName={row.app_name} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="sbi-rg-region-money">
|
||||
@ -394,8 +393,7 @@ export function RegionsView() {
|
||||
</td>
|
||||
<td className="is-left">
|
||||
<span className="sbi-rg-app-tag">
|
||||
<span className="sbi-rg-app-dot" style={{ background: appColor(row.app_code, appCodes) }} />
|
||||
{row.app_name}
|
||||
<SocialAppIdentity appCode={row.app_code} appName={row.app_name} />
|
||||
</span>
|
||||
</td>
|
||||
{TABLE_METRICS.map((metric) => (
|
||||
@ -414,7 +412,9 @@ export function RegionsView() {
|
||||
<td className="is-left">
|
||||
<span className="sbi-rg-child-name">{countryDisplayName(child.country_code, child.country_name)}</span>
|
||||
</td>
|
||||
<td className="is-left sbi-rg-child-app">{child.app_name}</td>
|
||||
<td className="is-left sbi-rg-child-app">
|
||||
<SocialAppIdentity appCode={child.app_code} appName={child.app_name} />
|
||||
</td>
|
||||
{TABLE_METRICS.map((metric) => (
|
||||
<td key={metric}>{formatMetric(metric, child[metric])}</td>
|
||||
))}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// 留存趋势(汇总/单 App)、App/区域留存热力对比表。数据契约见 SocialBiApp.jsx 的 useSocialBi()。
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useSocialBi } from "../SocialBiApp.jsx";
|
||||
import { SocialAppIdentity, useSocialBi } from "../SocialBiApp.jsx";
|
||||
import { EChart } from "../../charts/EChart.jsx";
|
||||
import { aggregateMetricMaps, formatMetric, metricChartValue, metricLabel, metricTooltip } from "../metrics.js";
|
||||
import { formatCount, formatPercentValue, formatRatioPercent, isBlank } from "../format.js";
|
||||
@ -204,7 +204,6 @@ export function RetentionView() {
|
||||
return tableSource
|
||||
.map((row) => ({
|
||||
key: tableDim === "region" ? `${row.app_code}:${row.region_id}` : row.app_code,
|
||||
label: tableDim === "region" ? `${row.app_name}·${row.region_name}` : row.app_name,
|
||||
row
|
||||
}))
|
||||
.sort((a, b) => (Number(b.row.new_users) || 0) - (Number(a.row.new_users) || 0));
|
||||
@ -312,7 +311,7 @@ export function RetentionView() {
|
||||
role="radio"
|
||||
type="button"
|
||||
>
|
||||
{row.app_name}
|
||||
<SocialAppIdentity appCode={row.app_code} appName={row.app_name} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@ -377,7 +376,10 @@ export function RetentionView() {
|
||||
</tr>
|
||||
{tableRows.map((item) => (
|
||||
<tr key={item.key}>
|
||||
<td className="is-left">{item.label}</td>
|
||||
<td className="is-left">
|
||||
<SocialAppIdentity appCode={item.row.app_code} appName={item.row.app_name} />
|
||||
{tableDim === "region" ? <span> · {item.row.region_name}</span> : null}
|
||||
</td>
|
||||
{TABLE_METRIC_KEYS.map((key) => renderMetricCell(key, item.row[key]))}
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@ -2,8 +2,7 @@
|
||||
// 金额字段为 USD 分,缺失值显示 "--",不在前端把缺失口径当作 0。
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useSocialBi } from "../SocialBiApp.jsx";
|
||||
import { appColor } from "../metrics.js";
|
||||
import { SocialAppIdentity, useSocialBi } from "../SocialBiApp.jsx";
|
||||
import { formatCount, formatMoneyMinor, isBlank } from "../format.js";
|
||||
import "./team-kpi-view.css";
|
||||
|
||||
@ -41,7 +40,7 @@ function operatorCountOf(items) {
|
||||
}
|
||||
|
||||
export function TeamKpiView() {
|
||||
const { appCodes, isLoading, kpi } = useSocialBi();
|
||||
const { isLoading, kpi } = useSocialBi();
|
||||
const [dimension, setDimension] = useState("person");
|
||||
|
||||
const items = useMemo(() => kpi?.items || [], [kpi]);
|
||||
@ -132,9 +131,9 @@ export function TeamKpiView() {
|
||||
</div>
|
||||
<div className="sbi-table-scroll sbi-kpi-board-scroll">
|
||||
{dimension === "person" ? (
|
||||
<PersonTable appCodes={appCodes} rows={personRows} />
|
||||
<PersonTable rows={personRows} />
|
||||
) : dimension === "operatorApp" ? (
|
||||
<OperatorAppTable appCodes={appCodes} rows={operatorAppRows} />
|
||||
<OperatorAppTable rows={operatorAppRows} />
|
||||
) : (
|
||||
<TeamTable rows={teamRows} />
|
||||
)}
|
||||
@ -163,7 +162,7 @@ function RankBadge({ rank }) {
|
||||
return <span className={`sbi-kpi-rank${medal}`}>{rank}</span>;
|
||||
}
|
||||
|
||||
function OperatorAppTable({ appCodes, rows }) {
|
||||
function OperatorAppTable({ rows }) {
|
||||
return (
|
||||
<table className="sbi-table">
|
||||
<thead>
|
||||
@ -196,7 +195,7 @@ function OperatorAppTable({ appCodes, rows }) {
|
||||
<OperatorCell item={item} />
|
||||
</td>
|
||||
<td className="is-left">
|
||||
<AppCell appCodes={appCodes} item={item} />
|
||||
<AppCell item={item} />
|
||||
</td>
|
||||
<td className="is-left">{item.whole_app_scope ? "全部区域" : item.region_names || `${formatCount(item.region_count)} 个区域`}</td>
|
||||
<td>{formatMoneyMinor(item.range_recharge_usd_minor)}</td>
|
||||
@ -209,7 +208,7 @@ function OperatorAppTable({ appCodes, rows }) {
|
||||
);
|
||||
}
|
||||
|
||||
function PersonTable({ appCodes, rows }) {
|
||||
function PersonTable({ rows }) {
|
||||
return (
|
||||
<table className="sbi-table">
|
||||
<thead>
|
||||
@ -242,7 +241,7 @@ function PersonTable({ appCodes, rows }) {
|
||||
<OperatorCell item={item} />
|
||||
</td>
|
||||
<td className="is-left">
|
||||
<AppCell appCodes={appCodes} item={item} />
|
||||
<AppCell item={item} />
|
||||
</td>
|
||||
<td className="is-left">{regionLabelOf(item)}</td>
|
||||
<td>{formatMoneyMinor(item.range_recharge_usd_minor)}</td>
|
||||
@ -312,11 +311,10 @@ function OperatorCell({ item }) {
|
||||
);
|
||||
}
|
||||
|
||||
function AppCell({ appCodes, item }) {
|
||||
function AppCell({ item }) {
|
||||
return (
|
||||
<span className="sbi-kpi-app">
|
||||
<span className="sbi-kpi-app-dot" style={{ background: appColor(item.app_code, appCodes) }} />
|
||||
{item.app_name || item.app_code}
|
||||
<SocialAppIdentity appCode={item.app_code} appName={item.app_name} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sbi-funnel-apps span {
|
||||
.sbi-funnel-apps > span {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--sbi-border);
|
||||
border-radius: 999px;
|
||||
|
||||
@ -101,13 +101,6 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.sbi-rg-app-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
flex: none;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.sbi-rg-region-money {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -62,13 +62,6 @@
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.sbi-kpi-app-dot {
|
||||
flex: none;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.sbi-kpi-table-skeleton {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
|
||||
@ -265,12 +265,6 @@
|
||||
box-shadow: 0 0 0 3px rgba(37, 87, 214, 0.1);
|
||||
}
|
||||
|
||||
.sbi-chip-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.sbi-region-select {
|
||||
position: relative;
|
||||
}
|
||||
@ -331,6 +325,9 @@
|
||||
}
|
||||
|
||||
.sbi-region-option.is-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--sbi-text-2);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
32
src/shared/api/generated/schema.d.ts
vendored
32
src/shared/api/generated/schema.d.ts
vendored
@ -4961,6 +4961,27 @@ export interface components {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
ApiResponseSocialBiMaster: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["SocialBiMasterResult"];
|
||||
};
|
||||
SocialBiMasterResult: {
|
||||
apps: components["schemas"]["SocialBiMasterApp"][];
|
||||
regions: {
|
||||
[key: string]: unknown;
|
||||
}[];
|
||||
operators: {
|
||||
[key: string]: unknown;
|
||||
}[];
|
||||
access: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
SocialBiMasterApp: {
|
||||
app_code: string;
|
||||
app_name: string;
|
||||
logo_url: string;
|
||||
kind: string;
|
||||
};
|
||||
ApiResponseSocialBiRequirements: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["SocialBiRequirementsResult"];
|
||||
};
|
||||
@ -6313,6 +6334,15 @@ export interface components {
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
SocialBiMasterResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApiResponseSocialBiMaster"];
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
SocialBiRequirementsResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@ -9456,7 +9486,7 @@ export interface operations {
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["StatisticsObjectResponse"];
|
||||
200: components["responses"]["SocialBiMasterResponse"];
|
||||
};
|
||||
};
|
||||
getSocialBiOverview: {
|
||||
|
||||
@ -3,9 +3,10 @@ import styles from "./AppIdentity.module.css";
|
||||
|
||||
// AppIdentity 是后台中 App 的唯一展示形式:业务页只传目录数据,避免各个下拉、表格和弹窗分别维护 Logo 规则。
|
||||
export function AppIdentity({ app, appCode, appName, className = "", logoUrl, size = "medium" }) {
|
||||
const resolvedCode = String(app?.appCode || appCode || "").trim();
|
||||
const resolvedName = String(app?.appName || appName || resolvedCode || "-").trim();
|
||||
const resolvedLogoUrl = String(app?.logoUrl || logoUrl || "").trim();
|
||||
// 主后台使用 camelCase,Social BI 的服务端聚合契约使用 snake_case;组件在展示边界统一兼容,业务视图不复制字段转换。
|
||||
const resolvedCode = String(app?.appCode || app?.app_code || appCode || "").trim();
|
||||
const resolvedName = String(app?.appName || app?.app_name || appName || resolvedCode || "-").trim();
|
||||
const resolvedLogoUrl = String(app?.logoUrl || app?.logo_url || logoUrl || "").trim();
|
||||
|
||||
return (
|
||||
<span className={[styles.root, styles[size] || styles.medium, className].filter(Boolean).join(" ")}>
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
.logo,
|
||||
.fallback {
|
||||
border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border, #d9e2ef) 80%, transparent);
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 10%);
|
||||
flex: 0 0 auto;
|
||||
@ -18,7 +18,7 @@
|
||||
.fallback {
|
||||
align-items: center;
|
||||
background: linear-gradient(145deg, #e8f1ff, #f7faff);
|
||||
color: var(--primary);
|
||||
color: var(--primary, #2557d6);
|
||||
display: inline-flex;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
|
||||
@ -20,3 +20,12 @@ test("falls back to a stable initial when the logo cannot load", () => {
|
||||
expect(container.querySelector("img")).toBeNull();
|
||||
expect(screen.getByText("L", { selector: "span" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("supports the snake case Social BI app contract", () => {
|
||||
const { container } = render(
|
||||
<AppIdentity app={{ app_code: "aslan", app_name: "Aslan", logo_url: "https://media.example.com/aslan.png" }} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Aslan")).toBeInTheDocument();
|
||||
expect(container.querySelector("img")).toHaveAttribute("src", "https://media.example.com/aslan.png");
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user