开屏配置
This commit is contained in:
parent
e423adc738
commit
0b3589a701
@ -760,6 +760,70 @@
|
|||||||
"x-permissions": ["app-config:update"]
|
"x-permissions": ["app-config:update"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/app-config/splash-screens": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "listSplashScreens",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/EmptyResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-permission": "app-config:view",
|
||||||
|
"x-permissions": ["app-config:view"]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"operationId": "createSplashScreen",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/EmptyResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-permission": "app-config:update",
|
||||||
|
"x-permissions": ["app-config:update"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/app-config/splash-screens/{splash_id}": {
|
||||||
|
"put": {
|
||||||
|
"operationId": "updateSplashScreen",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/EmptyResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "splash_id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-permission": "app-config:update",
|
||||||
|
"x-permissions": ["app-config:update"]
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"operationId": "deleteSplashScreen",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/EmptyResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "splash_id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-permission": "app-config:update",
|
||||||
|
"x-permissions": ["app-config:update"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/app-config/h5-links": {
|
"/admin/app-config/h5-links": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "listH5Links",
|
"operationId": "listH5Links",
|
||||||
|
|||||||
@ -146,6 +146,7 @@ export const fallbackNavigation = [
|
|||||||
children: [
|
children: [
|
||||||
routeNavItem("app-config-h5", { icon: SettingsApplicationsOutlined }),
|
routeNavItem("app-config-h5", { icon: SettingsApplicationsOutlined }),
|
||||||
routeNavItem("app-config-banners", { icon: ImageOutlined }),
|
routeNavItem("app-config-banners", { icon: ImageOutlined }),
|
||||||
|
routeNavItem("app-config-splash-screens", { icon: ImageOutlined }),
|
||||||
routeNavItem("app-config-explore", { icon: MapOutlined }),
|
routeNavItem("app-config-explore", { icon: MapOutlined }),
|
||||||
routeNavItem("payment-recharge-products", { icon: WalletOutlined }),
|
routeNavItem("payment-recharge-products", { icon: WalletOutlined }),
|
||||||
routeNavItem("app-config-versions", { icon: SettingsApplicationsOutlined }),
|
routeNavItem("app-config-versions", { icon: SettingsApplicationsOutlined }),
|
||||||
@ -357,7 +358,19 @@ export function mergeNavigationItems(primaryItems = [], fallbackItems = []) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return merged;
|
return orderNavigationItemsByFallback(merged, fallbackItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderNavigationItemsByFallback(items = [], fallbackItems = []) {
|
||||||
|
const fallbackOrder = new Map(fallbackItems.map((item, index) => [item.code, index]));
|
||||||
|
return items
|
||||||
|
.map((item, index) => ({ index, item }))
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftOrder = fallbackOrder.get(left.item.code) ?? Number.MAX_SAFE_INTEGER;
|
||||||
|
const rightOrder = fallbackOrder.get(right.item.code) ?? Number.MAX_SAFE_INTEGER;
|
||||||
|
return leftOrder === rightOrder ? left.index - right.index : leftOrder - rightOrder;
|
||||||
|
})
|
||||||
|
.map(({ item }) => item);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findNavItemByPath(pathname, items = fallbackNavigation) {
|
export function findNavItemByPath(pathname, items = fallbackNavigation) {
|
||||||
|
|||||||
@ -113,6 +113,56 @@ describe("navigation menu helpers", () => {
|
|||||||
);
|
);
|
||||||
expect(payment).toBeUndefined();
|
expect(payment).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("places splash screens after banners when backend menu is stale", () => {
|
||||||
|
const backendMenus = [
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{ code: "app-config-h5", id: "app-config-h5", label: "H5配置", path: "/app-config/h5" },
|
||||||
|
{
|
||||||
|
code: "app-config-banners",
|
||||||
|
id: "app-config-banners",
|
||||||
|
label: "BANNER配置",
|
||||||
|
path: "/app-config/banners",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "app-config-explore",
|
||||||
|
id: "app-config-explore",
|
||||||
|
label: "Explore配置",
|
||||||
|
path: "/app-config/explore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "payment-recharge-products",
|
||||||
|
id: "payment-recharge-products",
|
||||||
|
label: "内购配置",
|
||||||
|
path: "/app-config/recharge-products",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "app-config-versions",
|
||||||
|
id: "app-config-versions",
|
||||||
|
label: "版本管理",
|
||||||
|
path: "/app-config/versions",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
code: "app-config",
|
||||||
|
icon: "settings",
|
||||||
|
id: "app-config",
|
||||||
|
label: "APP配置",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const fallbackMenus = filterNavigationByPermission(fallbackNavigation, (code) => code === "app-config:view");
|
||||||
|
const merged = mergeNavigationItems(mapBackendMenus(backendMenus), fallbackMenus);
|
||||||
|
const appConfig = merged.find((item) => item.code === "app-config");
|
||||||
|
|
||||||
|
expect(appConfig.children.map((item) => item.code)).toEqual([
|
||||||
|
"app-config-h5",
|
||||||
|
"app-config-banners",
|
||||||
|
"app-config-splash-screens",
|
||||||
|
"app-config-explore",
|
||||||
|
"payment-recharge-products",
|
||||||
|
"app-config-versions",
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function flattenCodes(items) {
|
function flattenCodes(items) {
|
||||||
|
|||||||
@ -176,6 +176,7 @@ export const MENU_CODES = {
|
|||||||
appConfig: "app-config",
|
appConfig: "app-config",
|
||||||
appConfigH5: "app-config-h5",
|
appConfigH5: "app-config-h5",
|
||||||
appConfigBanners: "app-config-banners",
|
appConfigBanners: "app-config-banners",
|
||||||
|
appConfigSplashScreens: "app-config-splash-screens",
|
||||||
appConfigExplore: "app-config-explore",
|
appConfigExplore: "app-config-explore",
|
||||||
appConfigVersions: "app-config-versions",
|
appConfigVersions: "app-config-versions",
|
||||||
resources: "resources",
|
resources: "resources",
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import type {
|
|||||||
ApiList,
|
ApiList,
|
||||||
AppBannerDto,
|
AppBannerDto,
|
||||||
AppBannerPayload,
|
AppBannerPayload,
|
||||||
|
AppSplashScreenDto,
|
||||||
|
AppSplashScreenPayload,
|
||||||
AppVersionDto,
|
AppVersionDto,
|
||||||
AppVersionPayload,
|
AppVersionPayload,
|
||||||
EntityId,
|
EntityId,
|
||||||
@ -18,23 +20,26 @@ import type {
|
|||||||
export function listH5Links(): Promise<ApiList<H5LinkConfigDto>> {
|
export function listH5Links(): Promise<ApiList<H5LinkConfigDto>> {
|
||||||
const endpoint = API_ENDPOINTS.listH5Links;
|
const endpoint = API_ENDPOINTS.listH5Links;
|
||||||
return apiRequest<ApiList<H5LinkConfigDto>>(apiEndpointPath(API_OPERATIONS.listH5Links), {
|
return apiRequest<ApiList<H5LinkConfigDto>>(apiEndpointPath(API_OPERATIONS.listH5Links), {
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateH5Links(payload: H5LinkConfigUpdatePayload): Promise<ApiList<H5LinkConfigDto>> {
|
export function updateH5Links(payload: H5LinkConfigUpdatePayload): Promise<ApiList<H5LinkConfigDto>> {
|
||||||
const endpoint = API_ENDPOINTS.updateH5Links;
|
const endpoint = API_ENDPOINTS.updateH5Links;
|
||||||
return apiRequest<ApiList<H5LinkConfigDto>, H5LinkConfigUpdatePayload>(apiEndpointPath(API_OPERATIONS.updateH5Links), {
|
return apiRequest<ApiList<H5LinkConfigDto>, H5LinkConfigUpdatePayload>(
|
||||||
body: payload,
|
apiEndpointPath(API_OPERATIONS.updateH5Links),
|
||||||
method: endpoint.method
|
{
|
||||||
});
|
body: payload,
|
||||||
|
method: endpoint.method,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createH5Link(payload: H5LinkConfigPayload): Promise<H5LinkConfigDto> {
|
export function createH5Link(payload: H5LinkConfigPayload): Promise<H5LinkConfigDto> {
|
||||||
const endpoint = API_ENDPOINTS.createH5Link;
|
const endpoint = API_ENDPOINTS.createH5Link;
|
||||||
return apiRequest<H5LinkConfigDto, H5LinkConfigPayload>(apiEndpointPath(API_OPERATIONS.createH5Link), {
|
return apiRequest<H5LinkConfigDto, H5LinkConfigPayload>(apiEndpointPath(API_OPERATIONS.createH5Link), {
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,14 +47,14 @@ export function updateH5Link(key: EntityId, payload: H5LinkConfigPayload): Promi
|
|||||||
const endpoint = API_ENDPOINTS.updateH5Link;
|
const endpoint = API_ENDPOINTS.updateH5Link;
|
||||||
return apiRequest<H5LinkConfigDto, H5LinkConfigPayload>(apiEndpointPath(API_OPERATIONS.updateH5Link, { key }), {
|
return apiRequest<H5LinkConfigDto, H5LinkConfigPayload>(apiEndpointPath(API_OPERATIONS.updateH5Link, { key }), {
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteH5Link(key: EntityId): Promise<{ deleted: boolean }> {
|
export function deleteH5Link(key: EntityId): Promise<{ deleted: boolean }> {
|
||||||
const endpoint = API_ENDPOINTS.deleteH5Link;
|
const endpoint = API_ENDPOINTS.deleteH5Link;
|
||||||
return apiRequest<{ deleted: boolean }>(apiEndpointPath(API_OPERATIONS.deleteH5Link, { key }), {
|
return apiRequest<{ deleted: boolean }>(apiEndpointPath(API_OPERATIONS.deleteH5Link, { key }), {
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +62,7 @@ export function listExploreTabs(query: PageQuery = {}): Promise<ApiList<ExploreT
|
|||||||
const endpoint = API_ENDPOINTS.listExploreTabs;
|
const endpoint = API_ENDPOINTS.listExploreTabs;
|
||||||
return apiRequest<ApiList<ExploreTabDto>>(apiEndpointPath(API_OPERATIONS.listExploreTabs), {
|
return apiRequest<ApiList<ExploreTabDto>>(apiEndpointPath(API_OPERATIONS.listExploreTabs), {
|
||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
query
|
query,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,22 +70,25 @@ export function createExploreTab(payload: ExploreTabPayload): Promise<ExploreTab
|
|||||||
const endpoint = API_ENDPOINTS.createExploreTab;
|
const endpoint = API_ENDPOINTS.createExploreTab;
|
||||||
return apiRequest<ExploreTabDto, ExploreTabPayload>(apiEndpointPath(API_OPERATIONS.createExploreTab), {
|
return apiRequest<ExploreTabDto, ExploreTabPayload>(apiEndpointPath(API_OPERATIONS.createExploreTab), {
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateExploreTab(tabId: EntityId, payload: ExploreTabPayload): Promise<ExploreTabDto> {
|
export function updateExploreTab(tabId: EntityId, payload: ExploreTabPayload): Promise<ExploreTabDto> {
|
||||||
const endpoint = API_ENDPOINTS.updateExploreTab;
|
const endpoint = API_ENDPOINTS.updateExploreTab;
|
||||||
return apiRequest<ExploreTabDto, ExploreTabPayload>(apiEndpointPath(API_OPERATIONS.updateExploreTab, { tab_id: tabId }), {
|
return apiRequest<ExploreTabDto, ExploreTabPayload>(
|
||||||
body: payload,
|
apiEndpointPath(API_OPERATIONS.updateExploreTab, { tab_id: tabId }),
|
||||||
method: endpoint.method
|
{
|
||||||
});
|
body: payload,
|
||||||
|
method: endpoint.method,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteExploreTab(tabId: EntityId): Promise<{ deleted: boolean }> {
|
export function deleteExploreTab(tabId: EntityId): Promise<{ deleted: boolean }> {
|
||||||
const endpoint = API_ENDPOINTS.deleteExploreTab;
|
const endpoint = API_ENDPOINTS.deleteExploreTab;
|
||||||
return apiRequest<{ deleted: boolean }>(apiEndpointPath(API_OPERATIONS.deleteExploreTab, { tab_id: tabId }), {
|
return apiRequest<{ deleted: boolean }>(apiEndpointPath(API_OPERATIONS.deleteExploreTab, { tab_id: tabId }), {
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +96,7 @@ export function listBanners(query: PageQuery = {}): Promise<ApiList<AppBannerDto
|
|||||||
const endpoint = API_ENDPOINTS.listBanners;
|
const endpoint = API_ENDPOINTS.listBanners;
|
||||||
return apiRequest<ApiList<AppBannerDto>>(apiEndpointPath(API_OPERATIONS.listBanners), {
|
return apiRequest<ApiList<AppBannerDto>>(apiEndpointPath(API_OPERATIONS.listBanners), {
|
||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
query
|
query,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,30 +104,70 @@ export function createBanner(payload: AppBannerPayload): Promise<AppBannerDto> {
|
|||||||
const endpoint = API_ENDPOINTS.createBanner;
|
const endpoint = API_ENDPOINTS.createBanner;
|
||||||
return apiRequest<AppBannerDto, AppBannerPayload>(apiEndpointPath(API_OPERATIONS.createBanner), {
|
return apiRequest<AppBannerDto, AppBannerPayload>(apiEndpointPath(API_OPERATIONS.createBanner), {
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateBanner(bannerId: EntityId, payload: AppBannerPayload): Promise<AppBannerDto> {
|
export function updateBanner(bannerId: EntityId, payload: AppBannerPayload): Promise<AppBannerDto> {
|
||||||
const endpoint = API_ENDPOINTS.updateBanner;
|
const endpoint = API_ENDPOINTS.updateBanner;
|
||||||
return apiRequest<AppBannerDto, AppBannerPayload>(apiEndpointPath(API_OPERATIONS.updateBanner, { banner_id: bannerId }), {
|
return apiRequest<AppBannerDto, AppBannerPayload>(
|
||||||
body: payload,
|
apiEndpointPath(API_OPERATIONS.updateBanner, { banner_id: bannerId }),
|
||||||
method: endpoint.method
|
{
|
||||||
});
|
body: payload,
|
||||||
|
method: endpoint.method,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteBanner(bannerId: EntityId): Promise<{ deleted: boolean }> {
|
export function deleteBanner(bannerId: EntityId): Promise<{ deleted: boolean }> {
|
||||||
const endpoint = API_ENDPOINTS.deleteBanner;
|
const endpoint = API_ENDPOINTS.deleteBanner;
|
||||||
return apiRequest<{ deleted: boolean }>(apiEndpointPath(API_OPERATIONS.deleteBanner, { banner_id: bannerId }), {
|
return apiRequest<{ deleted: boolean }>(apiEndpointPath(API_OPERATIONS.deleteBanner, { banner_id: bannerId }), {
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listSplashScreens(query: PageQuery = {}): Promise<ApiList<AppSplashScreenDto>> {
|
||||||
|
const endpoint = API_ENDPOINTS.listSplashScreens;
|
||||||
|
return apiRequest<ApiList<AppSplashScreenDto>>(apiEndpointPath(API_OPERATIONS.listSplashScreens), {
|
||||||
|
method: endpoint.method,
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSplashScreen(payload: AppSplashScreenPayload): Promise<AppSplashScreenDto> {
|
||||||
|
const endpoint = API_ENDPOINTS.createSplashScreen;
|
||||||
|
return apiRequest<AppSplashScreenDto, AppSplashScreenPayload>(apiEndpointPath(API_OPERATIONS.createSplashScreen), {
|
||||||
|
body: payload,
|
||||||
|
method: endpoint.method,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSplashScreen(splashId: EntityId, payload: AppSplashScreenPayload): Promise<AppSplashScreenDto> {
|
||||||
|
const endpoint = API_ENDPOINTS.updateSplashScreen;
|
||||||
|
return apiRequest<AppSplashScreenDto, AppSplashScreenPayload>(
|
||||||
|
apiEndpointPath(API_OPERATIONS.updateSplashScreen, { splash_id: splashId }),
|
||||||
|
{
|
||||||
|
body: payload,
|
||||||
|
method: endpoint.method,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteSplashScreen(splashId: EntityId): Promise<{ deleted: boolean }> {
|
||||||
|
const endpoint = API_ENDPOINTS.deleteSplashScreen;
|
||||||
|
return apiRequest<{ deleted: boolean }>(
|
||||||
|
apiEndpointPath(API_OPERATIONS.deleteSplashScreen, { splash_id: splashId }),
|
||||||
|
{
|
||||||
|
method: endpoint.method,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function listAppVersions(query: PageQuery = {}): Promise<ApiList<AppVersionDto>> {
|
export function listAppVersions(query: PageQuery = {}): Promise<ApiList<AppVersionDto>> {
|
||||||
const endpoint = API_ENDPOINTS.listAppVersions;
|
const endpoint = API_ENDPOINTS.listAppVersions;
|
||||||
return apiRequest<ApiList<AppVersionDto>>(apiEndpointPath(API_OPERATIONS.listAppVersions), {
|
return apiRequest<ApiList<AppVersionDto>>(apiEndpointPath(API_OPERATIONS.listAppVersions), {
|
||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
query
|
query,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +175,7 @@ export function createAppVersion(payload: AppVersionPayload): Promise<AppVersion
|
|||||||
const endpoint = API_ENDPOINTS.createAppVersion;
|
const endpoint = API_ENDPOINTS.createAppVersion;
|
||||||
return apiRequest<AppVersionDto, AppVersionPayload>(apiEndpointPath(API_OPERATIONS.createAppVersion), {
|
return apiRequest<AppVersionDto, AppVersionPayload>(apiEndpointPath(API_OPERATIONS.createAppVersion), {
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,14 +185,17 @@ export function updateAppVersion(versionId: EntityId, payload: AppVersionPayload
|
|||||||
apiEndpointPath(API_OPERATIONS.updateAppVersion, { version_id: versionId }),
|
apiEndpointPath(API_OPERATIONS.updateAppVersion, { version_id: versionId }),
|
||||||
{
|
{
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteAppVersion(versionId: EntityId): Promise<{ deleted: boolean }> {
|
export function deleteAppVersion(versionId: EntityId): Promise<{ deleted: boolean }> {
|
||||||
const endpoint = API_ENDPOINTS.deleteAppVersion;
|
const endpoint = API_ENDPOINTS.deleteAppVersion;
|
||||||
return apiRequest<{ deleted: boolean }>(apiEndpointPath(API_OPERATIONS.deleteAppVersion, { version_id: versionId }), {
|
return apiRequest<{ deleted: boolean }>(
|
||||||
method: endpoint.method
|
apiEndpointPath(API_OPERATIONS.deleteAppVersion, { version_id: versionId }),
|
||||||
});
|
{
|
||||||
|
method: endpoint.method,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,11 +24,85 @@
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.splashCover,
|
||||||
|
.splashCoverEmpty {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 54px;
|
||||||
|
height: 96px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-card-strong);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.splashCover {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.formWideField {
|
.formWideField {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.splashFormLayout {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
grid-template-columns: minmax(220px, 260px) minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
gap: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.splashAssetPane,
|
||||||
|
.splashFieldPane {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.splashAssetPane {
|
||||||
|
padding-right: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.splashFieldPane {
|
||||||
|
padding-left: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.splashUploadField {
|
||||||
|
width: min(100%, 260px);
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splashUploadField .splashUploadPreview {
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 9 / 16;
|
||||||
|
max-height: min(52vh, 420px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.splashStatusField {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
.statusCell {
|
.statusCell {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.splashFormLayout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.splashAssetPane,
|
||||||
|
.splashFieldPane {
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splashUploadField {
|
||||||
|
justify-self: stretch;
|
||||||
|
width: min(100%, 260px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
204
src/features/app-config/hooks/useSplashScreenConfigPage.js
Normal file
204
src/features/app-config/hooks/useSplashScreenConfigPage.js
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
createSplashScreen,
|
||||||
|
deleteSplashScreen,
|
||||||
|
listSplashScreens,
|
||||||
|
updateSplashScreen,
|
||||||
|
} from "@/features/app-config/api";
|
||||||
|
import { useAppConfigAbilities } from "@/features/app-config/permissions.js";
|
||||||
|
import { appSplashScreenSchema } from "@/features/app-config/schema";
|
||||||
|
import { useCountryOptions } from "@/features/host-org/hooks/useCountryOptions.js";
|
||||||
|
import { parseForm } from "@/shared/forms/validation";
|
||||||
|
import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js";
|
||||||
|
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||||
|
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
||||||
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||||
|
|
||||||
|
const emptyData = { items: [], total: 0 };
|
||||||
|
|
||||||
|
const emptyForm = () => ({
|
||||||
|
countryCode: "",
|
||||||
|
coverUrl: "",
|
||||||
|
description: "",
|
||||||
|
endsAtMs: "",
|
||||||
|
param: "",
|
||||||
|
platform: "android",
|
||||||
|
regionId: "",
|
||||||
|
sortOrder: "0",
|
||||||
|
splashType: "h5",
|
||||||
|
startsAtMs: "",
|
||||||
|
status: "active",
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useSplashScreenConfigPage() {
|
||||||
|
const abilities = useAppConfigAbilities();
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const { countryOptions, loadingCountries } = useCountryOptions();
|
||||||
|
const { loadingRegions, regionOptions } = useRegionOptions();
|
||||||
|
const [keyword, setKeyword] = useState("");
|
||||||
|
const [status, setStatus] = useState("");
|
||||||
|
const [platform, setPlatform] = useState("");
|
||||||
|
const [regionId, setRegionId] = useState("");
|
||||||
|
const [countryCode, setCountryCode] = useState("");
|
||||||
|
const [activeAction, setActiveAction] = useState("");
|
||||||
|
const [editingItem, setEditingItem] = useState(null);
|
||||||
|
const [form, setFormState] = useState(emptyForm);
|
||||||
|
const [loadingAction, setLoadingAction] = useState("");
|
||||||
|
|
||||||
|
const filters = useMemo(
|
||||||
|
() => ({
|
||||||
|
countryCode,
|
||||||
|
keyword,
|
||||||
|
platform,
|
||||||
|
regionId,
|
||||||
|
status,
|
||||||
|
}),
|
||||||
|
[countryCode, keyword, platform, regionId, status],
|
||||||
|
);
|
||||||
|
const queryFn = useCallback(() => listSplashScreens(filters), [filters]);
|
||||||
|
const {
|
||||||
|
data = emptyData,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
reload,
|
||||||
|
} = useAdminQuery(queryFn, {
|
||||||
|
errorMessage: "加载开屏配置失败",
|
||||||
|
initialData: emptyData,
|
||||||
|
queryKey: ["app-config", "splash-screens", filters],
|
||||||
|
});
|
||||||
|
|
||||||
|
const setForm = (patch) => {
|
||||||
|
setFormState((current) => ({ ...current, ...patch }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
setEditingItem(null);
|
||||||
|
setFormState(emptyForm());
|
||||||
|
setActiveAction("create");
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEdit = (item) => {
|
||||||
|
setEditingItem(item);
|
||||||
|
setFormState(formFromSplashScreen(item));
|
||||||
|
setActiveAction("edit");
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
setActiveAction("");
|
||||||
|
setEditingItem(null);
|
||||||
|
setFormState(emptyForm());
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setKeyword("");
|
||||||
|
setStatus("");
|
||||||
|
setPlatform("");
|
||||||
|
setRegionId("");
|
||||||
|
setCountryCode("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitSplashScreen = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const payload = parseForm(appSplashScreenSchema, normalizeForm(form));
|
||||||
|
const action = editingItem ? "edit" : "create";
|
||||||
|
setLoadingAction(action);
|
||||||
|
try {
|
||||||
|
if (editingItem) {
|
||||||
|
await updateSplashScreen(editingItem.id, payload);
|
||||||
|
showToast("开屏配置已更新", "success");
|
||||||
|
} else {
|
||||||
|
await createSplashScreen(payload);
|
||||||
|
showToast("开屏配置已新增", "success");
|
||||||
|
}
|
||||||
|
closeDialog();
|
||||||
|
await reload();
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message || "操作失败", "error");
|
||||||
|
} finally {
|
||||||
|
setLoadingAction("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSplashScreen = async (item) => {
|
||||||
|
const ok = await confirm({
|
||||||
|
confirmText: "删除",
|
||||||
|
message: item.description || item.param || `开屏配置 ${item.id}`,
|
||||||
|
title: "删除开屏配置",
|
||||||
|
tone: "danger",
|
||||||
|
});
|
||||||
|
if (!ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoadingAction(`delete:${item.id}`);
|
||||||
|
try {
|
||||||
|
await deleteSplashScreen(item.id);
|
||||||
|
await reload();
|
||||||
|
showToast("开屏配置已删除", "success");
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message || "删除失败", "error");
|
||||||
|
} finally {
|
||||||
|
setLoadingAction("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
abilities,
|
||||||
|
activeAction,
|
||||||
|
closeDialog,
|
||||||
|
countryCode,
|
||||||
|
countryOptions,
|
||||||
|
data,
|
||||||
|
editingItem,
|
||||||
|
error,
|
||||||
|
form,
|
||||||
|
keyword,
|
||||||
|
loading,
|
||||||
|
loadingAction,
|
||||||
|
loadingCountries,
|
||||||
|
loadingRegions,
|
||||||
|
openCreate,
|
||||||
|
openEdit,
|
||||||
|
platform,
|
||||||
|
regionId,
|
||||||
|
regionOptions,
|
||||||
|
reload,
|
||||||
|
removeSplashScreen,
|
||||||
|
resetFilters,
|
||||||
|
setCountryCode,
|
||||||
|
setForm,
|
||||||
|
setKeyword,
|
||||||
|
setPlatform,
|
||||||
|
setRegionId,
|
||||||
|
setStatus,
|
||||||
|
status,
|
||||||
|
submitSplashScreen,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeForm(form) {
|
||||||
|
return {
|
||||||
|
...form,
|
||||||
|
countryCode: form.countryCode || "",
|
||||||
|
endsAtMs: form.endsAtMs || 0,
|
||||||
|
regionId: form.regionId || 0,
|
||||||
|
sortOrder: form.sortOrder || 0,
|
||||||
|
startsAtMs: form.startsAtMs || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formFromSplashScreen(item) {
|
||||||
|
return {
|
||||||
|
countryCode: item.countryCode || "",
|
||||||
|
coverUrl: item.coverUrl || "",
|
||||||
|
description: item.description || "",
|
||||||
|
endsAtMs: item.endsAtMs || "",
|
||||||
|
param: item.param || "",
|
||||||
|
platform: item.platform || "android",
|
||||||
|
regionId: item.regionId ? String(item.regionId) : "",
|
||||||
|
sortOrder: String(item.sortOrder || 0),
|
||||||
|
splashType: item.splashType || "h5",
|
||||||
|
startsAtMs: item.startsAtMs || "",
|
||||||
|
status: item.status || "active",
|
||||||
|
};
|
||||||
|
}
|
||||||
425
src/features/app-config/pages/SplashScreenConfigPage.jsx
Normal file
425
src/features/app-config/pages/SplashScreenConfigPage.jsx
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
import AddOutlined from "@mui/icons-material/AddOutlined";
|
||||||
|
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
||||||
|
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||||||
|
import ImageOutlined from "@mui/icons-material/ImageOutlined";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useSplashScreenConfigPage } from "@/features/app-config/hooks/useSplashScreenConfigPage.js";
|
||||||
|
import styles from "@/features/app-config/app-config.module.css";
|
||||||
|
import {
|
||||||
|
AdminFormDialog,
|
||||||
|
AdminFormFieldGrid,
|
||||||
|
AdminFormSection,
|
||||||
|
AdminFormSwitchField,
|
||||||
|
} from "@/shared/ui/AdminFormDialog.jsx";
|
||||||
|
import {
|
||||||
|
AdminActionIconButton,
|
||||||
|
AdminListBody,
|
||||||
|
AdminListPage,
|
||||||
|
AdminListToolbar,
|
||||||
|
AdminRowActions,
|
||||||
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
|
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
||||||
|
import { RegionSelect } from "@/shared/ui/RegionSelect.jsx";
|
||||||
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
|
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
|
||||||
|
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
||||||
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
["", "全部状态"],
|
||||||
|
["active", "启用"],
|
||||||
|
["disabled", "关闭"],
|
||||||
|
["expired", "过期"],
|
||||||
|
];
|
||||||
|
|
||||||
|
const platformOptions = [
|
||||||
|
["", "全部平台"],
|
||||||
|
["android", "安卓"],
|
||||||
|
["ios", "iOS"],
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SplashScreenConfigPage() {
|
||||||
|
const page = useSplashScreenConfigPage();
|
||||||
|
const items = page.data.items || [];
|
||||||
|
const columns = useMemo(() => splashScreenColumns(page), [page]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminListPage>
|
||||||
|
<AdminListToolbar
|
||||||
|
actions={
|
||||||
|
page.abilities.canUpdate ? (
|
||||||
|
<AdminActionIconButton label="新增开屏" primary onClick={page.openCreate}>
|
||||||
|
<AddOutlined fontSize="small" />
|
||||||
|
</AdminActionIconButton>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
|
<AdminListBody>
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
items={items}
|
||||||
|
minWidth="1320px"
|
||||||
|
pagination={{
|
||||||
|
itemCount: items.length,
|
||||||
|
page: 1,
|
||||||
|
pageSize: items.length || 1,
|
||||||
|
total: page.data.total ?? items.length,
|
||||||
|
}}
|
||||||
|
rowKey={(item) => item.id}
|
||||||
|
/>
|
||||||
|
</AdminListBody>
|
||||||
|
</DataState>
|
||||||
|
|
||||||
|
<AdminFormDialog
|
||||||
|
loading={page.loadingAction === "create" || page.loadingAction === "edit"}
|
||||||
|
open={Boolean(page.activeAction)}
|
||||||
|
size="large"
|
||||||
|
submitDisabled={
|
||||||
|
!page.abilities.canUpdate || page.loadingAction === "create" || page.loadingAction === "edit"
|
||||||
|
}
|
||||||
|
title={page.editingItem ? "编辑开屏配置" : "新增开屏配置"}
|
||||||
|
onClose={page.closeDialog}
|
||||||
|
onSubmit={page.submitSplashScreen}
|
||||||
|
>
|
||||||
|
<div className={styles.splashFormLayout}>
|
||||||
|
<div className={styles.splashAssetPane}>
|
||||||
|
<AdminFormSection title="素材">
|
||||||
|
<UploadField
|
||||||
|
className={styles.splashUploadField}
|
||||||
|
disabled={!page.abilities.canUpdate || !page.abilities.canUpload}
|
||||||
|
label="封面图"
|
||||||
|
previewClassName={styles.splashUploadPreview}
|
||||||
|
value={page.form.coverUrl}
|
||||||
|
onChange={(coverUrl) => page.setForm({ coverUrl })}
|
||||||
|
/>
|
||||||
|
</AdminFormSection>
|
||||||
|
</div>
|
||||||
|
<div className={styles.splashFieldPane}>
|
||||||
|
<AdminFormSection title="跳转信息">
|
||||||
|
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
||||||
|
<TextField
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
label="类型"
|
||||||
|
required
|
||||||
|
select
|
||||||
|
value={page.form.splashType}
|
||||||
|
onChange={(event) => page.setForm({ splashType: event.target.value })}
|
||||||
|
>
|
||||||
|
<MenuItem value="h5">H5</MenuItem>
|
||||||
|
<MenuItem value="app">APP</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
label="平台"
|
||||||
|
required
|
||||||
|
select
|
||||||
|
value={page.form.platform}
|
||||||
|
onChange={(event) => page.setForm({ platform: event.target.value })}
|
||||||
|
>
|
||||||
|
<MenuItem value="android">安卓</MenuItem>
|
||||||
|
<MenuItem value="ios">iOS</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
className={styles.formWideField}
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
label={page.form.splashType === "h5" ? "参数(H5链接)" : "参数"}
|
||||||
|
required={page.form.splashType === "h5"}
|
||||||
|
value={page.form.param}
|
||||||
|
onChange={(event) => page.setForm({ param: event.target.value })}
|
||||||
|
/>
|
||||||
|
</AdminFormFieldGrid>
|
||||||
|
</AdminFormSection>
|
||||||
|
<AdminFormSection title="投放设置">
|
||||||
|
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
||||||
|
<RegionSelect
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
emptyLabel="全部区域"
|
||||||
|
loading={page.loadingRegions}
|
||||||
|
options={page.regionOptions}
|
||||||
|
value={page.form.regionId}
|
||||||
|
onChange={(regionId) => page.setForm({ regionId })}
|
||||||
|
/>
|
||||||
|
<CountrySelect
|
||||||
|
disabled={!page.abilities.canUpdate || page.loadingCountries}
|
||||||
|
emptyLabel="全部国家"
|
||||||
|
label="国家"
|
||||||
|
options={page.countryOptions}
|
||||||
|
value={page.form.countryCode}
|
||||||
|
onChange={(countryCode) => page.setForm({ countryCode })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
label="排序"
|
||||||
|
slotProps={{ htmlInput: { step: 1 } }}
|
||||||
|
type="number"
|
||||||
|
value={page.form.sortOrder}
|
||||||
|
onChange={(event) => page.setForm({ sortOrder: event.target.value })}
|
||||||
|
/>
|
||||||
|
<AdminFormSwitchField
|
||||||
|
checked={page.form.status === "active"}
|
||||||
|
checkedLabel="启用"
|
||||||
|
className={styles.splashStatusField}
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
label="开屏状态"
|
||||||
|
switchProps={{ inputProps: { "aria-label": "开屏启用状态" } }}
|
||||||
|
uncheckedLabel={page.form.status === "expired" ? "过期" : "关闭"}
|
||||||
|
onChange={(checked) => page.setForm({ status: checked ? "active" : "disabled" })}
|
||||||
|
/>
|
||||||
|
<TimeRangeFilter
|
||||||
|
className={styles.formWideField}
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
label="投放时间区间"
|
||||||
|
value={{ endMs: page.form.endsAtMs, startMs: page.form.startsAtMs }}
|
||||||
|
onChange={(range) =>
|
||||||
|
page.setForm({ endsAtMs: range.endMs, startsAtMs: range.startMs })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</AdminFormFieldGrid>
|
||||||
|
</AdminFormSection>
|
||||||
|
<AdminFormSection title="描述">
|
||||||
|
<TextField
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
label="描述"
|
||||||
|
multiline
|
||||||
|
minRows={2}
|
||||||
|
value={page.form.description}
|
||||||
|
onChange={(event) => page.setForm({ description: event.target.value })}
|
||||||
|
/>
|
||||||
|
</AdminFormSection>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AdminFormDialog>
|
||||||
|
</AdminListPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function splashScreenColumns(page) {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
key: "cover",
|
||||||
|
label: "封面图",
|
||||||
|
render: (item) => <SplashCover src={item.coverUrl} />,
|
||||||
|
width: "minmax(96px, 0.45fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "target",
|
||||||
|
label: "类型 / 参数",
|
||||||
|
render: (item) => <Stack primary={typeLabel(item.splashType)} secondary={item.param || "-"} />,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索参数、国家、描述",
|
||||||
|
value: page.keyword,
|
||||||
|
onChange: page.setKeyword,
|
||||||
|
}),
|
||||||
|
width: "minmax(260px, 1.3fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "platform",
|
||||||
|
label: "平台",
|
||||||
|
render: (item) => platformLabel(item.platform),
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: platformOptions,
|
||||||
|
placeholder: "搜索平台",
|
||||||
|
value: page.platform,
|
||||||
|
onChange: page.setPlatform,
|
||||||
|
}),
|
||||||
|
width: "minmax(110px, 0.6fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "status",
|
||||||
|
label: "状态",
|
||||||
|
render: (item) => <StatusBadge status={item.status} />,
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: statusOptions,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: page.status,
|
||||||
|
onChange: page.setStatus,
|
||||||
|
}),
|
||||||
|
width: "minmax(100px, 0.55fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "region",
|
||||||
|
label: "区域",
|
||||||
|
render: (item) => regionLabel(page.regionOptions, item.regionId),
|
||||||
|
filter: createRegionColumnFilter({
|
||||||
|
loading: page.loadingRegions,
|
||||||
|
options: page.regionOptions,
|
||||||
|
value: page.regionId,
|
||||||
|
onChange: page.setRegionId,
|
||||||
|
}),
|
||||||
|
width: "minmax(150px, 0.8fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "country",
|
||||||
|
label: "国家",
|
||||||
|
render: (item) => countryLabel(page.countryOptions, item.countryCode),
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
emptyLabel: "全部国家",
|
||||||
|
loading: page.loadingCountries,
|
||||||
|
options: page.countryOptions,
|
||||||
|
placeholder: "搜索国家",
|
||||||
|
value: page.countryCode,
|
||||||
|
onChange: page.setCountryCode,
|
||||||
|
}),
|
||||||
|
width: "minmax(150px, 0.8fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "sortOrder",
|
||||||
|
label: "排序",
|
||||||
|
width: "minmax(80px, 0.4fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "deliveryTime",
|
||||||
|
label: "投放时间",
|
||||||
|
render: (item) => deliveryTimeLabel(item.startsAtMs, item.endsAtMs),
|
||||||
|
width: "minmax(220px, 1fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "description",
|
||||||
|
label: "描述",
|
||||||
|
render: (item) => item.description || "-",
|
||||||
|
width: "minmax(180px, 0.9fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "updatedAtMs",
|
||||||
|
label: "更新时间",
|
||||||
|
render: (item) => formatMillis(item.updatedAtMs),
|
||||||
|
width: "minmax(160px, 0.8fr)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!page.abilities.canUpdate) {
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
...columns,
|
||||||
|
{
|
||||||
|
key: "actions",
|
||||||
|
label: "操作",
|
||||||
|
render: (item) => <SplashActions item={item} page={page} />,
|
||||||
|
width: "minmax(96px, 0.45fr)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function SplashCover({ src }) {
|
||||||
|
if (!src) {
|
||||||
|
return (
|
||||||
|
<span className={styles.splashCoverEmpty}>
|
||||||
|
<ImageOutlined fontSize="small" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <img alt="" className={styles.splashCover} src={src} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SplashActions({ item, page }) {
|
||||||
|
const deleting = page.loadingAction === `delete:${item.id}`;
|
||||||
|
return (
|
||||||
|
<AdminRowActions>
|
||||||
|
<AdminActionIconButton
|
||||||
|
disabled={Boolean(page.loadingAction)}
|
||||||
|
label="编辑"
|
||||||
|
onClick={() => page.openEdit(item)}
|
||||||
|
>
|
||||||
|
<EditOutlined fontSize="small" />
|
||||||
|
</AdminActionIconButton>
|
||||||
|
<AdminActionIconButton
|
||||||
|
disabled={deleting || Boolean(page.loadingAction)}
|
||||||
|
label="删除"
|
||||||
|
onClick={() => page.removeSplashScreen(item)}
|
||||||
|
>
|
||||||
|
<DeleteOutlineOutlined fontSize="small" />
|
||||||
|
</AdminActionIconButton>
|
||||||
|
</AdminRowActions>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CountrySelect({ className, disabled, emptyLabel, label = "国家", onChange, options, value }) {
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
className={className}
|
||||||
|
disabled={disabled}
|
||||||
|
label={label}
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
value={value || ""}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="">{emptyLabel}</MenuItem>
|
||||||
|
{options.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Stack({ primary, secondary }) {
|
||||||
|
return (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<span>{primary || "-"}</span>
|
||||||
|
<span className="muted">{secondary || "-"}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge({ status }) {
|
||||||
|
const tone = status === "active" ? "succeeded" : status === "expired" ? "warning" : "stopped";
|
||||||
|
return (
|
||||||
|
<span className={`status-badge status-badge--${tone}`}>
|
||||||
|
<span className="status-point" />
|
||||||
|
{statusLabel(status)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeLabel(value) {
|
||||||
|
return value === "app" ? "APP" : "H5";
|
||||||
|
}
|
||||||
|
|
||||||
|
function platformLabel(value) {
|
||||||
|
if (value === "android") {
|
||||||
|
return "安卓";
|
||||||
|
}
|
||||||
|
if (value === "ios") {
|
||||||
|
return "iOS";
|
||||||
|
}
|
||||||
|
return value || "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
function deliveryTimeLabel(startsAtMs, endsAtMs) {
|
||||||
|
const start = startsAtMs ? formatMillis(startsAtMs) : "不限";
|
||||||
|
const end = endsAtMs ? formatMillis(endsAtMs) : "不限";
|
||||||
|
return `${start} - ${end}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status) {
|
||||||
|
if (status === "active") {
|
||||||
|
return "启用";
|
||||||
|
}
|
||||||
|
if (status === "expired") {
|
||||||
|
return "过期";
|
||||||
|
}
|
||||||
|
return "关闭";
|
||||||
|
}
|
||||||
|
|
||||||
|
function regionLabel(options, value) {
|
||||||
|
if (!value) {
|
||||||
|
return "全部区域";
|
||||||
|
}
|
||||||
|
return options.find((option) => option.value === String(value))?.label || `区域 ${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countryLabel(options, value) {
|
||||||
|
if (!value) {
|
||||||
|
return "全部国家";
|
||||||
|
}
|
||||||
|
return options.find((option) => option.value === value)?.label || value;
|
||||||
|
}
|
||||||
@ -1,45 +1,53 @@
|
|||||||
import { MENU_CODES, PERMISSIONS } from "@/app/permissions";
|
import { MENU_CODES, PERMISSIONS } from "@/app/permissions";
|
||||||
|
|
||||||
export const appConfigRoutes = [
|
export const appConfigRoutes = [
|
||||||
{
|
{
|
||||||
label: "H5配置",
|
label: "H5配置",
|
||||||
loader: () => import("./pages/H5ConfigPage.jsx").then((module) => module.H5ConfigPage),
|
loader: () => import("./pages/H5ConfigPage.jsx").then((module) => module.H5ConfigPage),
|
||||||
menuCode: MENU_CODES.appConfigH5,
|
menuCode: MENU_CODES.appConfigH5,
|
||||||
pageKey: "app-config-h5",
|
pageKey: "app-config-h5",
|
||||||
path: "/app-config/h5",
|
path: "/app-config/h5",
|
||||||
permission: PERMISSIONS.appConfigView
|
permission: PERMISSIONS.appConfigView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "BANNER配置",
|
label: "BANNER配置",
|
||||||
loader: () => import("./pages/BannerConfigPage.jsx").then((module) => module.BannerConfigPage),
|
loader: () => import("./pages/BannerConfigPage.jsx").then((module) => module.BannerConfigPage),
|
||||||
menuCode: MENU_CODES.appConfigBanners,
|
menuCode: MENU_CODES.appConfigBanners,
|
||||||
pageKey: "app-config-banners",
|
pageKey: "app-config-banners",
|
||||||
path: "/app-config/banners",
|
path: "/app-config/banners",
|
||||||
permission: PERMISSIONS.appConfigView
|
permission: PERMISSIONS.appConfigView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Explore配置",
|
label: "开屏配置",
|
||||||
loader: () => import("./pages/ExploreConfigPage.jsx").then((module) => module.ExploreConfigPage),
|
loader: () => import("./pages/SplashScreenConfigPage.jsx").then((module) => module.SplashScreenConfigPage),
|
||||||
menuCode: MENU_CODES.appConfigExplore,
|
menuCode: MENU_CODES.appConfigSplashScreens,
|
||||||
pageKey: "app-config-explore",
|
pageKey: "app-config-splash-screens",
|
||||||
path: "/app-config/explore",
|
path: "/app-config/splash-screens",
|
||||||
permission: PERMISSIONS.appConfigView
|
permission: PERMISSIONS.appConfigView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "内购配置",
|
label: "Explore配置",
|
||||||
loader: () =>
|
loader: () => import("./pages/ExploreConfigPage.jsx").then((module) => module.ExploreConfigPage),
|
||||||
import("../payment/pages/RechargeProductConfigPage.jsx").then((module) => module.RechargeProductConfigPage),
|
menuCode: MENU_CODES.appConfigExplore,
|
||||||
menuCode: MENU_CODES.paymentRechargeProducts,
|
pageKey: "app-config-explore",
|
||||||
pageKey: "payment-recharge-products",
|
path: "/app-config/explore",
|
||||||
path: "/app-config/recharge-products",
|
permission: PERMISSIONS.appConfigView,
|
||||||
permission: PERMISSIONS.paymentProductView
|
},
|
||||||
},
|
{
|
||||||
{
|
label: "内购配置",
|
||||||
label: "版本管理",
|
loader: () =>
|
||||||
loader: () => import("./pages/AppVersionManagementPage.jsx").then((module) => module.AppVersionManagementPage),
|
import("../payment/pages/RechargeProductConfigPage.jsx").then((module) => module.RechargeProductConfigPage),
|
||||||
menuCode: MENU_CODES.appConfigVersions,
|
menuCode: MENU_CODES.paymentRechargeProducts,
|
||||||
pageKey: "app-config-versions",
|
pageKey: "payment-recharge-products",
|
||||||
path: "/app-config/versions",
|
path: "/app-config/recharge-products",
|
||||||
permission: PERMISSIONS.appVersionView
|
permission: PERMISSIONS.paymentProductView,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
label: "版本管理",
|
||||||
|
loader: () => import("./pages/AppVersionManagementPage.jsx").then((module) => module.AppVersionManagementPage),
|
||||||
|
menuCode: MENU_CODES.appConfigVersions,
|
||||||
|
pageKey: "app-config-versions",
|
||||||
|
path: "/app-config/versions",
|
||||||
|
permission: PERMISSIONS.appVersionView,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
import { appBannerSchema, exploreTabSchema, h5LinkUpdateSchema } from "@/features/app-config/schema";
|
import {
|
||||||
|
appBannerSchema,
|
||||||
|
appSplashScreenSchema,
|
||||||
|
exploreTabSchema,
|
||||||
|
h5LinkUpdateSchema,
|
||||||
|
} from "@/features/app-config/schema";
|
||||||
import { FormValidationError, parseForm } from "@/shared/forms/validation";
|
import { FormValidationError, parseForm } from "@/shared/forms/validation";
|
||||||
|
|
||||||
const validBannerForm = {
|
const validBannerForm = {
|
||||||
@ -19,6 +24,20 @@ const validBannerForm = {
|
|||||||
status: "active",
|
status: "active",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validSplashScreenForm = {
|
||||||
|
countryCode: "",
|
||||||
|
coverUrl: "https://media.haiyihy.com/splash/launch.png",
|
||||||
|
description: "",
|
||||||
|
endsAtMs: "1800000000000",
|
||||||
|
param: "https://h5.example.com/splash",
|
||||||
|
platform: "ios",
|
||||||
|
regionId: "",
|
||||||
|
sortOrder: "0",
|
||||||
|
splashType: "h5",
|
||||||
|
startsAtMs: "1700000000000",
|
||||||
|
status: "active",
|
||||||
|
};
|
||||||
|
|
||||||
describe("app config form schema", () => {
|
describe("app config form schema", () => {
|
||||||
test("validates dynamic h5 config item", () => {
|
test("validates dynamic h5 config item", () => {
|
||||||
const payload = parseForm(h5LinkUpdateSchema, {
|
const payload = parseForm(h5LinkUpdateSchema, {
|
||||||
@ -79,6 +98,34 @@ describe("app config form schema", () => {
|
|||||||
).toThrow(FormValidationError);
|
).toThrow(FormValidationError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("keeps splash screen delivery and placement fields without display scope", () => {
|
||||||
|
const payload = parseForm(appSplashScreenSchema, validSplashScreenForm);
|
||||||
|
|
||||||
|
expect(payload.splashType).toBe("h5");
|
||||||
|
expect(payload.startsAtMs).toBe(1700000000000);
|
||||||
|
expect(payload.endsAtMs).toBe(1800000000000);
|
||||||
|
expect("displayScopes" in payload).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("requires h5 link for splash screen h5 type", () => {
|
||||||
|
expect(() =>
|
||||||
|
parseForm(appSplashScreenSchema, {
|
||||||
|
...validSplashScreenForm,
|
||||||
|
param: "",
|
||||||
|
}),
|
||||||
|
).toThrow(FormValidationError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects inverted splash screen delivery time range", () => {
|
||||||
|
expect(() =>
|
||||||
|
parseForm(appSplashScreenSchema, {
|
||||||
|
...validSplashScreenForm,
|
||||||
|
endsAtMs: "1700000000000",
|
||||||
|
startsAtMs: "1800000000000",
|
||||||
|
}),
|
||||||
|
).toThrow(FormValidationError);
|
||||||
|
});
|
||||||
|
|
||||||
test("validates explore tab h5 url and sort order", () => {
|
test("validates explore tab h5 url and sort order", () => {
|
||||||
const payload = parseForm(exploreTabSchema, {
|
const payload = parseForm(exploreTabSchema, {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@ -84,6 +84,43 @@ export const appBannerSchema = z
|
|||||||
|
|
||||||
export type AppBannerForm = z.infer<typeof appBannerSchema>;
|
export type AppBannerForm = z.infer<typeof appBannerSchema>;
|
||||||
|
|
||||||
|
export const appSplashScreenSchema = z
|
||||||
|
.object({
|
||||||
|
countryCode: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.transform((value) => value.toUpperCase())
|
||||||
|
.refine((value) => value === "" || /^[A-Z]{2,3}$/.test(value), "国家编码需为 2-3 位字母"),
|
||||||
|
coverUrl: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "请上传封面图")
|
||||||
|
.max(1024, "封面图地址不能超过 1024 个字符")
|
||||||
|
.refine((value) => !/\s/.test(value), "封面图地址不能包含空白字符"),
|
||||||
|
description: z.string().trim().max(255, "描述不能超过 255 个字符"),
|
||||||
|
endsAtMs: z.coerce.number().int().min(0, "投放结束时间不正确").default(0),
|
||||||
|
param: z.string().trim().max(2048, "参数不能超过 2048 个字符"),
|
||||||
|
platform: z.enum(["android", "ios"]),
|
||||||
|
regionId: z.coerce.number().int().min(0, "区域不正确").default(0),
|
||||||
|
sortOrder: z.coerce.number().int("排序必须是整数").default(0),
|
||||||
|
splashType: z.enum(["h5", "app"]),
|
||||||
|
startsAtMs: z.coerce.number().int().min(0, "投放开始时间不正确").default(0),
|
||||||
|
status: z.enum(["active", "disabled", "expired"]),
|
||||||
|
})
|
||||||
|
.superRefine((value, ctx) => {
|
||||||
|
if (value.splashType === "h5" && !value.param) {
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "H5 类型需要填写链接", path: ["param"] });
|
||||||
|
}
|
||||||
|
if (value.splashType === "h5" && /\s/.test(value.param)) {
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "H5 链接不能包含空白字符", path: ["param"] });
|
||||||
|
}
|
||||||
|
if (value.startsAtMs > 0 && value.endsAtMs > 0 && value.startsAtMs >= value.endsAtMs) {
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "投放结束时间必须晚于开始时间", path: ["endsAtMs"] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AppSplashScreenForm = z.infer<typeof appSplashScreenSchema>;
|
||||||
|
|
||||||
export const appVersionSchema = z.object({
|
export const appVersionSchema = z.object({
|
||||||
buildNumber: z.coerce.number().int("编译版本号必须是整数").positive("编译版本号必须大于 0"),
|
buildNumber: z.coerce.number().int("编译版本号必须是整数").positive("编译版本号必须大于 0"),
|
||||||
description: z.string().trim().max(1000, "更新描述不能超过 1000 个字符").default(""),
|
description: z.string().trim().max(1000, "更新描述不能超过 1000 个字符").default(""),
|
||||||
|
|||||||
@ -50,6 +50,7 @@ export const API_OPERATIONS = {
|
|||||||
createResourceGroup: "createResourceGroup",
|
createResourceGroup: "createResourceGroup",
|
||||||
createRole: "createRole",
|
createRole: "createRole",
|
||||||
createRoomPin: "createRoomPin",
|
createRoomPin: "createRoomPin",
|
||||||
|
createSplashScreen: "createSplashScreen",
|
||||||
createTaskDefinition: "createTaskDefinition",
|
createTaskDefinition: "createTaskDefinition",
|
||||||
createTeamSalaryPolicy: "createTeamSalaryPolicy",
|
createTeamSalaryPolicy: "createTeamSalaryPolicy",
|
||||||
createTier: "createTier",
|
createTier: "createTier",
|
||||||
@ -71,6 +72,7 @@ export const API_OPERATIONS = {
|
|||||||
deleteRechargeProduct: "deleteRechargeProduct",
|
deleteRechargeProduct: "deleteRechargeProduct",
|
||||||
deleteRole: "deleteRole",
|
deleteRole: "deleteRole",
|
||||||
deleteRoom: "deleteRoom",
|
deleteRoom: "deleteRoom",
|
||||||
|
deleteSplashScreen: "deleteSplashScreen",
|
||||||
deleteTeamSalaryPolicy: "deleteTeamSalaryPolicy",
|
deleteTeamSalaryPolicy: "deleteTeamSalaryPolicy",
|
||||||
disableCountry: "disableCountry",
|
disableCountry: "disableCountry",
|
||||||
disableGift: "disableGift",
|
disableGift: "disableGift",
|
||||||
@ -155,6 +157,7 @@ export const API_OPERATIONS = {
|
|||||||
listRooms: "listRooms",
|
listRooms: "listRooms",
|
||||||
listRoomTurnoverRewardSettlements: "listRoomTurnoverRewardSettlements",
|
listRoomTurnoverRewardSettlements: "listRoomTurnoverRewardSettlements",
|
||||||
listSevenDayCheckInClaims: "listSevenDayCheckInClaims",
|
listSevenDayCheckInClaims: "listSevenDayCheckInClaims",
|
||||||
|
listSplashScreens: "listSplashScreens",
|
||||||
listSystemMenus: "listSystemMenus",
|
listSystemMenus: "listSystemMenus",
|
||||||
listTaskDefinitions: "listTaskDefinitions",
|
listTaskDefinitions: "listTaskDefinitions",
|
||||||
listTeamSalaryPolicies: "listTeamSalaryPolicies",
|
listTeamSalaryPolicies: "listTeamSalaryPolicies",
|
||||||
@ -220,6 +223,7 @@ export const API_OPERATIONS = {
|
|||||||
updateRoomRocketConfig: "updateRoomRocketConfig",
|
updateRoomRocketConfig: "updateRoomRocketConfig",
|
||||||
updateRoomTurnoverRewardConfig: "updateRoomTurnoverRewardConfig",
|
updateRoomTurnoverRewardConfig: "updateRoomTurnoverRewardConfig",
|
||||||
updateSevenDayCheckInConfig: "updateSevenDayCheckInConfig",
|
updateSevenDayCheckInConfig: "updateSevenDayCheckInConfig",
|
||||||
|
updateSplashScreen: "updateSplashScreen",
|
||||||
updateTaskDefinition: "updateTaskDefinition",
|
updateTaskDefinition: "updateTaskDefinition",
|
||||||
updateTeamSalaryPolicy: "updateTeamSalaryPolicy",
|
updateTeamSalaryPolicy: "updateTeamSalaryPolicy",
|
||||||
updateTier: "updateTier",
|
updateTier: "updateTier",
|
||||||
@ -494,6 +498,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "room-pin:create",
|
permission: "room-pin:create",
|
||||||
permissions: ["room-pin:create"]
|
permissions: ["room-pin:create"]
|
||||||
},
|
},
|
||||||
|
createSplashScreen: {
|
||||||
|
method: "POST",
|
||||||
|
operationId: API_OPERATIONS.createSplashScreen,
|
||||||
|
path: "/v1/admin/app-config/splash-screens",
|
||||||
|
permission: "app-config:update",
|
||||||
|
permissions: ["app-config:update"]
|
||||||
|
},
|
||||||
createTaskDefinition: {
|
createTaskDefinition: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createTaskDefinition,
|
operationId: API_OPERATIONS.createTaskDefinition,
|
||||||
@ -641,6 +652,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "room:delete",
|
permission: "room:delete",
|
||||||
permissions: ["room:delete"]
|
permissions: ["room:delete"]
|
||||||
},
|
},
|
||||||
|
deleteSplashScreen: {
|
||||||
|
method: "DELETE",
|
||||||
|
operationId: API_OPERATIONS.deleteSplashScreen,
|
||||||
|
path: "/v1/admin/app-config/splash-screens/{splash_id}",
|
||||||
|
permission: "app-config:update",
|
||||||
|
permissions: ["app-config:update"]
|
||||||
|
},
|
||||||
deleteTeamSalaryPolicy: {
|
deleteTeamSalaryPolicy: {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
operationId: API_OPERATIONS.deleteTeamSalaryPolicy,
|
operationId: API_OPERATIONS.deleteTeamSalaryPolicy,
|
||||||
@ -1227,6 +1245,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "seven-day-checkin:view",
|
permission: "seven-day-checkin:view",
|
||||||
permissions: ["seven-day-checkin:view"]
|
permissions: ["seven-day-checkin:view"]
|
||||||
},
|
},
|
||||||
|
listSplashScreens: {
|
||||||
|
method: "GET",
|
||||||
|
operationId: API_OPERATIONS.listSplashScreens,
|
||||||
|
path: "/v1/admin/app-config/splash-screens",
|
||||||
|
permission: "app-config:view",
|
||||||
|
permissions: ["app-config:view"]
|
||||||
|
},
|
||||||
listSystemMenus: {
|
listSystemMenus: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listSystemMenus,
|
operationId: API_OPERATIONS.listSystemMenus,
|
||||||
@ -1670,6 +1695,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "seven-day-checkin:update",
|
permission: "seven-day-checkin:update",
|
||||||
permissions: ["seven-day-checkin:update"]
|
permissions: ["seven-day-checkin:update"]
|
||||||
},
|
},
|
||||||
|
updateSplashScreen: {
|
||||||
|
method: "PUT",
|
||||||
|
operationId: API_OPERATIONS.updateSplashScreen,
|
||||||
|
path: "/v1/admin/app-config/splash-screens/{splash_id}",
|
||||||
|
permission: "app-config:update",
|
||||||
|
permissions: ["app-config:update"]
|
||||||
|
},
|
||||||
updateTaskDefinition: {
|
updateTaskDefinition: {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
operationId: API_OPERATIONS.updateTaskDefinition,
|
operationId: API_OPERATIONS.updateTaskDefinition,
|
||||||
|
|||||||
84
src/shared/api/generated/schema.d.ts
vendored
84
src/shared/api/generated/schema.d.ts
vendored
@ -596,6 +596,38 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/admin/app-config/splash-screens": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["listSplashScreens"];
|
||||||
|
put?: never;
|
||||||
|
post: operations["createSplashScreen"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/admin/app-config/splash-screens/{splash_id}": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put: operations["updateSplashScreen"];
|
||||||
|
post?: never;
|
||||||
|
delete: operations["deleteSplashScreen"];
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/admin/app-config/h5-links": {
|
"/admin/app-config/h5-links": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -3574,6 +3606,58 @@ export interface operations {
|
|||||||
200: components["responses"]["EmptyResponse"];
|
200: components["responses"]["EmptyResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
listSplashScreens: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["EmptyResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
createSplashScreen: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["EmptyResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
updateSplashScreen: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
splash_id: number;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["EmptyResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
deleteSplashScreen: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
splash_id: number;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["EmptyResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
listH5Links: {
|
listH5Links: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
@ -800,6 +800,38 @@ export interface AppBannerPayload {
|
|||||||
status: "active" | "disabled" | "expired";
|
status: "active" | "disabled" | "expired";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSplashScreenDto {
|
||||||
|
appCode?: string;
|
||||||
|
countryCode?: string;
|
||||||
|
coverUrl: string;
|
||||||
|
createdAtMs?: number;
|
||||||
|
description?: string;
|
||||||
|
endsAtMs?: number;
|
||||||
|
id: number;
|
||||||
|
param?: string;
|
||||||
|
platform: "android" | "ios" | string;
|
||||||
|
regionId?: number;
|
||||||
|
sortOrder?: number;
|
||||||
|
splashType: "h5" | "app" | string;
|
||||||
|
startsAtMs?: number;
|
||||||
|
status: "active" | "disabled" | "expired" | string;
|
||||||
|
updatedAtMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppSplashScreenPayload {
|
||||||
|
countryCode?: string;
|
||||||
|
coverUrl: string;
|
||||||
|
description?: string;
|
||||||
|
endsAtMs?: number;
|
||||||
|
param?: string;
|
||||||
|
platform: "android" | "ios";
|
||||||
|
regionId?: number;
|
||||||
|
sortOrder?: number;
|
||||||
|
splashType: "h5" | "app";
|
||||||
|
startsAtMs?: number;
|
||||||
|
status: "active" | "disabled" | "expired";
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgencyMembershipDto {
|
export interface AgencyMembershipDto {
|
||||||
agencyId?: number;
|
agencyId?: number;
|
||||||
createdAtMs?: number;
|
createdAtMs?: number;
|
||||||
|
|||||||
@ -15,12 +15,15 @@ let pagModulePromise;
|
|||||||
|
|
||||||
export function UploadField({
|
export function UploadField({
|
||||||
accept,
|
accept,
|
||||||
|
className,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
density = "regular",
|
density = "regular",
|
||||||
kind = "image",
|
kind = "image",
|
||||||
label,
|
label,
|
||||||
onChange,
|
onChange,
|
||||||
onFileSelected,
|
onFileSelected,
|
||||||
|
panelClassName,
|
||||||
|
previewClassName,
|
||||||
uploadKind,
|
uploadKind,
|
||||||
value = "",
|
value = "",
|
||||||
}) {
|
}) {
|
||||||
@ -90,7 +93,12 @@ export function UploadField({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[styles.root, density === "compact" ? styles.compact : "", disabled ? styles.disabled : ""]
|
className={[
|
||||||
|
styles.root,
|
||||||
|
density === "compact" ? styles.compact : "",
|
||||||
|
disabled ? styles.disabled : "",
|
||||||
|
className || "",
|
||||||
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" ")}
|
.join(" ")}
|
||||||
>
|
>
|
||||||
@ -98,10 +106,10 @@ export function UploadField({
|
|||||||
<span className={styles.label}>{label}</span>
|
<span className={styles.label}>{label}</span>
|
||||||
<span className={styles.type}>{assetKindLabel(assetKind, isImage)}</span>
|
<span className={styles.type}>{assetKindLabel(assetKind, isImage)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.panel}>
|
<div className={[styles.panel, panelClassName || ""].filter(Boolean).join(" ")}>
|
||||||
<button
|
<button
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
className={styles.preview}
|
className={[styles.preview, previewClassName || ""].filter(Boolean).join(" ")}
|
||||||
disabled={disabled || uploading}
|
disabled={disabled || uploading}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={openPicker}
|
onClick={openPicker}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user