feat: update admin dashboard and baishun config
This commit is contained in:
parent
d0fcfe2151
commit
3a3c5a4bf9
@ -1,5 +1,41 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export interface CountryDashboardMetric {
|
||||
countryCode: string;
|
||||
countryName: string;
|
||||
countryNewUser: number;
|
||||
dealerRecharge: number | string;
|
||||
gamePayout: number | string;
|
||||
gameProfit: number | string;
|
||||
gameTotalFlow: number | string;
|
||||
giftConsume: number | string;
|
||||
luckyGiftAnchorShare: number | string;
|
||||
luckyGiftPayout: number | string;
|
||||
luckyGiftProfit: number | string;
|
||||
luckyGiftTotalFlow: number | string;
|
||||
newUserRecharge: number | string;
|
||||
officialRecharge: number | string;
|
||||
periodKey: string;
|
||||
periodName: string;
|
||||
salaryExchange: number | string;
|
||||
totalRecharge: number | string;
|
||||
}
|
||||
|
||||
export interface CountryDashboardResult {
|
||||
computedAt: string;
|
||||
endDate?: string;
|
||||
periodType: string;
|
||||
records: CountryDashboardMetric[];
|
||||
startDate?: string;
|
||||
total: CountryDashboardMetric;
|
||||
}
|
||||
|
||||
export async function countryDashboard(params: Record<string, any> = {}) {
|
||||
return requestClient.get<CountryDashboardResult>('/datav/country-dashboard', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function latestActiveUserCountryCode(params: Record<string, any> = {}) {
|
||||
return requestClient.get<Array<Record<string, any>>>(
|
||||
'/datav/active/user-country-code',
|
||||
|
||||
@ -36,6 +36,8 @@ function createProviderForm(sysOrigin = '') {
|
||||
sysOrigin,
|
||||
profile: 'PROD',
|
||||
platformBaseUrl: '',
|
||||
gameListUrl: '',
|
||||
luckyGiftUrl: '',
|
||||
appId: undefined as any,
|
||||
appName: '',
|
||||
appChannel: '',
|
||||
@ -85,6 +87,16 @@ const activeProfileLabel = computed(
|
||||
activeProfile.value,
|
||||
);
|
||||
|
||||
function defaultGameListUrl(platformBaseUrl: any) {
|
||||
const baseUrl = String(platformBaseUrl || '').trim().replace(/\/+$/, '');
|
||||
return baseUrl ? `${baseUrl}/v1/api/gamelist` : '';
|
||||
}
|
||||
|
||||
function defaultLuckyGiftUrl(platformBaseUrl: any) {
|
||||
const baseUrl = String(platformBaseUrl || '').trim().replace(/\/+$/, '');
|
||||
return baseUrl ? `${baseUrl}/lucky_gift/start_game` : '';
|
||||
}
|
||||
|
||||
const catalogColumns = [
|
||||
{ dataIndex: 'cover', key: 'cover', title: '封面', width: 90 },
|
||||
{ dataIndex: 'name', key: 'name', title: '名称', width: 180 },
|
||||
@ -138,6 +150,9 @@ function applyProviderForm(record: Record<string, any>) {
|
||||
activeProfile.value = String(record.activeProfile || activeProfile.value || 'PROD');
|
||||
Object.assign(providerForm, createProviderForm(contextQuery.sysOrigin), {
|
||||
...record,
|
||||
gameListUrl: record.gameListUrl || defaultGameListUrl(record.platformBaseUrl),
|
||||
luckyGiftUrl:
|
||||
record.luckyGiftUrl || defaultLuckyGiftUrl(record.platformBaseUrl),
|
||||
profile: contextQuery.profile,
|
||||
sysOrigin: contextQuery.sysOrigin,
|
||||
appId: Number(record.appId || 0) || undefined,
|
||||
@ -164,14 +179,24 @@ async function loadProviderConfig() {
|
||||
}
|
||||
|
||||
async function persistProviderConfig(showSuccess = true) {
|
||||
const gameListUrl =
|
||||
String(providerForm.gameListUrl || '').trim() ||
|
||||
defaultGameListUrl(providerForm.platformBaseUrl);
|
||||
const luckyGiftUrl =
|
||||
String(providerForm.luckyGiftUrl || '').trim() ||
|
||||
defaultLuckyGiftUrl(providerForm.platformBaseUrl);
|
||||
providerForm.gameListUrl = gameListUrl;
|
||||
providerForm.luckyGiftUrl = luckyGiftUrl;
|
||||
if (
|
||||
!contextQuery.sysOrigin ||
|
||||
!providerForm.platformBaseUrl ||
|
||||
!gameListUrl ||
|
||||
!luckyGiftUrl ||
|
||||
!providerForm.appId ||
|
||||
!providerForm.appChannel ||
|
||||
!providerForm.appKey
|
||||
) {
|
||||
message.warning('请补全百顺平台地址、AppId、AppChannel、AppKey');
|
||||
message.warning('请补全百顺平台地址、游戏列表地址、幸运礼物地址、AppId、AppChannel、AppKey');
|
||||
return false;
|
||||
}
|
||||
providerSaving.value = true;
|
||||
@ -322,6 +347,24 @@ async function handleImportSelected() {
|
||||
placeholder="https://game-cn-test.jieyou.shop"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="游戏列表地址"
|
||||
extra="获取百顺游戏列表时直接请求这个完整地址。"
|
||||
>
|
||||
<Input
|
||||
v-model:value="providerForm.gameListUrl"
|
||||
placeholder="https://game-cn-test.jieyou.shop/v1/api/gamelist"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="幸运礼物地址"
|
||||
extra="幸运礼物抽奖时直接请求这个完整地址。"
|
||||
>
|
||||
<Input
|
||||
v-model:value="providerForm.luckyGiftUrl"
|
||||
placeholder="https://game-cn-test.jieyou.shop/lucky_gift/start_game"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="AppId">
|
||||
<InputNumber
|
||||
v-model:value="providerForm.appId"
|
||||
|
||||
@ -76,6 +76,7 @@ function createProviderForm(sysOrigin = '') {
|
||||
return {
|
||||
sysOrigin,
|
||||
platformBaseUrl: '',
|
||||
gameListUrl: '',
|
||||
appId: undefined as any,
|
||||
appName: '',
|
||||
appChannel: '',
|
||||
@ -109,6 +110,12 @@ const categoryOptions = GAME_CATEGORY_OPTIONS.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.value as any,
|
||||
}));
|
||||
|
||||
function defaultGameListUrl(platformBaseUrl: any) {
|
||||
const baseUrl = String(platformBaseUrl || '').trim().replace(/\/+$/, '');
|
||||
return baseUrl ? `${baseUrl}/v1/api/gamelist` : '';
|
||||
}
|
||||
|
||||
const launchModeOptions = [
|
||||
{ label: 'H5_REMOTE', value: 'H5_REMOTE' },
|
||||
{ label: 'H5_ZIP_LOCAL', value: 'H5_ZIP_LOCAL' },
|
||||
@ -222,6 +229,7 @@ function resetGameForm() {
|
||||
function applyProviderForm(record: Record<string, any>) {
|
||||
Object.assign(providerForm, createProviderForm(contextQuery.sysOrigin), {
|
||||
...record,
|
||||
gameListUrl: record.gameListUrl || defaultGameListUrl(record.platformBaseUrl),
|
||||
sysOrigin: contextQuery.sysOrigin,
|
||||
appId: Number(record.appId || 0) || undefined,
|
||||
gsp: Number(record.gsp || 101) || 101,
|
||||
@ -248,14 +256,19 @@ async function loadProviderConfig() {
|
||||
}
|
||||
|
||||
async function persistProviderConfig(showSuccess = true) {
|
||||
const gameListUrl =
|
||||
String(providerForm.gameListUrl || '').trim() ||
|
||||
defaultGameListUrl(providerForm.platformBaseUrl);
|
||||
providerForm.gameListUrl = gameListUrl;
|
||||
if (
|
||||
!contextQuery.sysOrigin ||
|
||||
!providerForm.platformBaseUrl ||
|
||||
!gameListUrl ||
|
||||
!providerForm.appId ||
|
||||
!providerForm.appChannel ||
|
||||
!providerForm.appKey
|
||||
) {
|
||||
message.warning('请补全百顺平台地址、AppId、AppChannel、AppKey');
|
||||
message.warning('请补全百顺平台地址、游戏列表地址、AppId、AppChannel、AppKey');
|
||||
return false;
|
||||
}
|
||||
providerSaving.value = true;
|
||||
@ -458,6 +471,15 @@ async function handleImportSelected() {
|
||||
placeholder="https://game-cn-test.jieyou.shop"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="游戏列表地址"
|
||||
extra="获取百顺游戏列表时直接请求这个完整地址。"
|
||||
>
|
||||
<Input
|
||||
v-model:value="providerForm.gameListUrl"
|
||||
placeholder="https://game-cn-test.jieyou.shop/v1/api/gamelist"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="AppId">
|
||||
<InputNumber
|
||||
v-model:value="providerForm.appId"
|
||||
|
||||
@ -1,9 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import type {
|
||||
CountryDashboardResult,
|
||||
} from '#/api/legacy/datav';
|
||||
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||
|
||||
import { useWindowSize } from '@vueuse/core';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Empty,
|
||||
Input,
|
||||
message,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
Tooltip,
|
||||
} from 'antdv-next';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { countryDashboard } from '#/api/legacy/datav';
|
||||
import SysOriginSelect from '#/components/sys-origin-select.vue';
|
||||
import { getAllowedSysOrigins } from '#/views/system/shared';
|
||||
|
||||
import LatestPurchaseList from './components/latest-purchase-list.vue';
|
||||
@ -12,17 +31,83 @@ import PurchaseCount from './components/purchase-count.vue';
|
||||
defineOptions({ name: 'StatisticsDatav' });
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const { width } = useWindowSize();
|
||||
|
||||
const datavFullscreen = ref<HTMLElement>();
|
||||
const fullscreen = ref(false);
|
||||
const loading = ref(false);
|
||||
const dashboard = ref<CountryDashboardResult | null>(null);
|
||||
const dateRange = ref<[string, string] | null>([
|
||||
dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
||||
dayjs().format('YYYY-MM-DD'),
|
||||
]);
|
||||
|
||||
const query = reactive({
|
||||
countryKeyword: '',
|
||||
periodType: 'DAY',
|
||||
sysOrigin: '',
|
||||
});
|
||||
|
||||
const mobile = computed(() => width.value < 768);
|
||||
const accessCodes = computed(() => accessStore.accessCodes || []);
|
||||
const permissionsSysOriginCode = computed(() => {
|
||||
const sysOriginOptions = computed(() => {
|
||||
const options = getAllowedSysOrigins(accessCodes.value);
|
||||
const finalOptions = options.length > 0 ? options : getAllowedSysOrigins([]);
|
||||
return finalOptions.map((item) => String(item.value || ''));
|
||||
return options.length > 0 ? options : getAllowedSysOrigins([]);
|
||||
});
|
||||
const permissionsSysOriginCode = computed(() =>
|
||||
sysOriginOptions.value.map((item) => String(item.value || '')),
|
||||
);
|
||||
|
||||
const periodTypeOptions = [
|
||||
{ label: '日', value: 'DAY' },
|
||||
{ label: '周', value: 'WEEK' },
|
||||
{ label: '月', value: 'MONTH' },
|
||||
{ label: '全部', value: 'ALL' },
|
||||
];
|
||||
|
||||
const metricColumns = new Set([
|
||||
'countryNewUser',
|
||||
'dealerRecharge',
|
||||
'gamePayout',
|
||||
'gameProfit',
|
||||
'gameTotalFlow',
|
||||
'giftConsume',
|
||||
'luckyGiftPayout',
|
||||
'luckyGiftProfit',
|
||||
'luckyGiftTotalFlow',
|
||||
'newUserRecharge',
|
||||
'salaryExchange',
|
||||
'totalRecharge',
|
||||
]);
|
||||
|
||||
const columns: any[] = [
|
||||
{ dataIndex: 'countryName', fixed: 'left' as const, key: 'countryName', title: '国家', width: 150 },
|
||||
{ dataIndex: 'countryCode', key: 'countryCode', title: 'Code', width: 86 },
|
||||
{ dataIndex: 'periodName', key: 'periodName', title: '周期', width: 110 },
|
||||
{ align: 'right', dataIndex: 'countryNewUser', key: 'countryNewUser', title: '新增', width: 110 },
|
||||
{ align: 'right', dataIndex: 'newUserRecharge', key: 'newUserRecharge', title: '新增充值', width: 130 },
|
||||
{ align: 'right', dataIndex: 'dealerRecharge', key: 'dealerRecharge', title: '币商充值', width: 130 },
|
||||
{ align: 'right', dataIndex: 'salaryExchange', key: 'salaryExchange', title: '工资兑换', width: 130 },
|
||||
{ align: 'right', dataIndex: 'totalRecharge', key: 'totalRecharge', title: '总充值', width: 130 },
|
||||
{ align: 'right', dataIndex: 'giftConsume', key: 'giftConsume', title: '礼物消耗', width: 130 },
|
||||
{ align: 'right', dataIndex: 'luckyGiftTotalFlow', key: 'luckyGiftTotalFlow', title: '幸运礼物流水', width: 150 },
|
||||
{ align: 'right', dataIndex: 'luckyGiftPayout', key: 'luckyGiftPayout', title: '幸运礼物返奖', width: 150 },
|
||||
{ align: 'right', dataIndex: 'luckyGiftProfit', key: 'luckyGiftProfit', title: '幸运礼物利润', width: 150 },
|
||||
{ align: 'right', dataIndex: 'gameTotalFlow', key: 'gameTotalFlow', title: '游戏流水', width: 130 },
|
||||
{ align: 'right', dataIndex: 'gamePayout', key: 'gamePayout', title: '游戏返奖', width: 130 },
|
||||
{ align: 'right', dataIndex: 'gameProfit', key: 'gameProfit', title: '游戏利润', width: 130 },
|
||||
];
|
||||
|
||||
const total = computed(() => dashboard.value?.total || null);
|
||||
const records = computed(() => dashboard.value?.records || []);
|
||||
const summaryCards = computed(() => {
|
||||
const item = total.value;
|
||||
return [
|
||||
{ label: '新增用户', value: formatInteger(item?.countryNewUser) },
|
||||
{ label: '总充值', value: formatAmount(item?.totalRecharge) },
|
||||
{ label: '礼物消耗', value: formatAmount(item?.giftConsume) },
|
||||
{ label: '幸运礼物利润', value: formatAmount(item?.luckyGiftProfit) },
|
||||
{ label: '游戏流水', value: formatAmount(item?.gameTotalFlow) },
|
||||
{ label: '游戏利润', value: formatAmount(item?.gameProfit) },
|
||||
];
|
||||
});
|
||||
|
||||
function hasPermission(code: string) {
|
||||
@ -52,8 +137,94 @@ async function screen() {
|
||||
await element.requestFullscreen();
|
||||
}
|
||||
|
||||
function defaultRange(periodType: string): [string, string] | null {
|
||||
const now = dayjs();
|
||||
if (periodType === 'ALL') {
|
||||
return null;
|
||||
}
|
||||
if (periodType === 'MONTH') {
|
||||
return [now.startOf('month').format('YYYY-MM-DD'), now.format('YYYY-MM-DD')];
|
||||
}
|
||||
if (periodType === 'WEEK') {
|
||||
const daysSinceMonday = now.day() === 0 ? 6 : now.day() - 1;
|
||||
return [now.subtract(daysSinceMonday, 'day').format('YYYY-MM-DD'), now.format('YYYY-MM-DD')];
|
||||
}
|
||||
return [now.subtract(6, 'day').format('YYYY-MM-DD'), now.format('YYYY-MM-DD')];
|
||||
}
|
||||
|
||||
function handlePeriodChange(value: string) {
|
||||
dateRange.value = defaultRange(value);
|
||||
loadDashboard();
|
||||
}
|
||||
|
||||
function buildParams() {
|
||||
const params: Record<string, any> = {
|
||||
countryKeyword: query.countryKeyword || undefined,
|
||||
periodType: query.periodType,
|
||||
sysOrigin: query.sysOrigin || undefined,
|
||||
};
|
||||
if (dateRange.value?.[0] && dateRange.value?.[1]) {
|
||||
params.startDate = dateRange.value[0];
|
||||
params.endDate = dateRange.value[1];
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
async function loadDashboard() {
|
||||
if (!canQuery.value) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
dashboard.value = await countryDashboard(buildParams());
|
||||
} catch (error) {
|
||||
dashboard.value = null;
|
||||
message.error('数据大屏加载失败');
|
||||
throw error;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatInteger(value?: null | number | string) {
|
||||
const number = Number(value || 0);
|
||||
return Number.isFinite(number) ? number.toLocaleString('en-US', { maximumFractionDigits: 0 }) : '0';
|
||||
}
|
||||
|
||||
function formatAmount(value?: null | number | string) {
|
||||
const number = Number(value || 0);
|
||||
return Number.isFinite(number)
|
||||
? number.toLocaleString('en-US', {
|
||||
maximumFractionDigits: 2,
|
||||
minimumFractionDigits: 0,
|
||||
})
|
||||
: '0';
|
||||
}
|
||||
|
||||
function tableCellText(record: Record<string, any>, key: string) {
|
||||
if (key === 'countryNewUser') {
|
||||
return formatInteger(record.countryNewUser);
|
||||
}
|
||||
return formatAmount(record[key] as number | string);
|
||||
}
|
||||
|
||||
function rowKey(record: Record<string, any>) {
|
||||
return `${record.countryCode}-${record.periodKey}`;
|
||||
}
|
||||
|
||||
watch(
|
||||
sysOriginOptions,
|
||||
(options) => {
|
||||
if (!query.sysOrigin && options[0]?.value) {
|
||||
query.sysOrigin = String(options[0].value);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('fullscreenchange', syncFullscreenState);
|
||||
loadDashboard();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@ -65,33 +236,115 @@ onBeforeUnmount(() => {
|
||||
<div class="app-container-datav">
|
||||
<div
|
||||
ref="datavFullscreen"
|
||||
:class="[
|
||||
'office-efficiency-index',
|
||||
fullscreen ? 'office-efficiency-index--fullscreen' : '',
|
||||
class="datav-shell" :class="[
|
||||
fullscreen ? 'datav-shell--fullscreen' : '',
|
||||
]"
|
||||
@dblclick="screen"
|
||||
>
|
||||
<div v-if="!mobile" class="office-header">
|
||||
<div class="title-info">数据大屏</div>
|
||||
<div class="datav-header">
|
||||
<div>
|
||||
<div class="datav-title">数据大屏</div>
|
||||
<div class="datav-subtitle">
|
||||
国家维度 · {{ dashboard?.computedAt || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<Space wrap>
|
||||
<Button ghost @click="screen">
|
||||
{{ fullscreen ? '退出全屏' : '全屏' }}
|
||||
</Button>
|
||||
<Button :loading="loading" type="primary" @click="loadDashboard">
|
||||
查询
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div class="analysis-info">实时数据预览</div>
|
||||
|
||||
<div v-if="canQuery" class="office-content">
|
||||
<div class="office-content__left">
|
||||
<section class="col-info">
|
||||
<div class="title">购买用户</div>
|
||||
<div class="content">
|
||||
<LatestPurchaseList :sys-origin-codes="permissionsSysOriginCode" />
|
||||
</div>
|
||||
<div v-if="canQuery" class="datav-content">
|
||||
<section class="panel filter-panel">
|
||||
<Space wrap>
|
||||
<SysOriginSelect
|
||||
v-model:value="query.sysOrigin"
|
||||
:options="sysOriginOptions"
|
||||
allow-clear
|
||||
placeholder="系统"
|
||||
style="width: 150px"
|
||||
@change="loadDashboard"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="query.periodType"
|
||||
:options="periodTypeOptions"
|
||||
style="width: 110px"
|
||||
@change="handlePeriodChange"
|
||||
/>
|
||||
<DateRangePicker
|
||||
v-model:value="dateRange"
|
||||
allow-clear
|
||||
style="width: 260px"
|
||||
value-format="YYYY-MM-DD"
|
||||
@change="loadDashboard"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="query.countryKeyword"
|
||||
allow-clear
|
||||
placeholder="国家编码 / 名称"
|
||||
style="width: 180px"
|
||||
@press-enter="loadDashboard"
|
||||
/>
|
||||
<Button :loading="loading" type="primary" @click="loadDashboard">
|
||||
查询
|
||||
</Button>
|
||||
</Space>
|
||||
</section>
|
||||
|
||||
<div class="summary-grid">
|
||||
<section
|
||||
v-for="item in summaryCards"
|
||||
:key="item.label"
|
||||
class="summary-card"
|
||||
>
|
||||
<div class="summary-card__label">{{ item.label }}</div>
|
||||
<div class="summary-card__value">{{ item.value }}</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="office-content__right">
|
||||
<section v-if="canPaidPreview" class="col-info">
|
||||
<div class="title">今日付费预览</div>
|
||||
<div class="content">
|
||||
<PurchaseCount />
|
||||
</div>
|
||||
<section class="panel dashboard-panel">
|
||||
<Spin :spinning="loading">
|
||||
<Table
|
||||
v-if="records.length > 0"
|
||||
:columns="columns"
|
||||
:data-source="records"
|
||||
:pagination="{ pageSize: 20, showSizeChanger: true }"
|
||||
:row-key="rowKey"
|
||||
:scroll="{ x: 1900 }"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="metricColumns.has(String(column.dataIndex))">
|
||||
<Tooltip v-if="column.dataIndex === 'luckyGiftProfit'">
|
||||
<template #title>
|
||||
主播分成:{{ formatAmount(record.luckyGiftAnchorShare) }}
|
||||
</template>
|
||||
<span class="number-cell">
|
||||
{{ tableCellText(record, String(column.dataIndex)) }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<span v-else class="number-cell">
|
||||
{{ tableCellText(record, String(column.dataIndex)) }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<Empty v-else />
|
||||
</Spin>
|
||||
</section>
|
||||
|
||||
<div class="legacy-grid">
|
||||
<section class="panel">
|
||||
<div class="panel-title">购买用户</div>
|
||||
<LatestPurchaseList :sys-origin-codes="permissionsSysOriginCode" />
|
||||
</section>
|
||||
<section v-if="canPaidPreview" class="panel">
|
||||
<div class="panel-title">今日付费预览</div>
|
||||
<PurchaseCount />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@ -101,114 +354,156 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped>
|
||||
.app-container-datav {
|
||||
background-color: #22284a;
|
||||
border-radius: 20px;
|
||||
background: #111827;
|
||||
min-height: calc(100vh - 120px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.office-efficiency-index {
|
||||
.datav-shell {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(49, 86, 173, 0.35), transparent 40%),
|
||||
radial-gradient(circle at top right, rgba(3, 194, 236, 0.12), transparent 36%),
|
||||
#22284a;
|
||||
color: #fff;
|
||||
linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(17, 24, 39, 0.98)),
|
||||
#111827;
|
||||
color: #f8fafc;
|
||||
min-height: inherit;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.office-efficiency-index--fullscreen {
|
||||
border-radius: 0;
|
||||
.datav-shell--fullscreen {
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.office-header {
|
||||
background: url('../../assets/datav/office_efficiency_header_bg.png')
|
||||
no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
padding: 18px 20px 10px;
|
||||
.datav-header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title-info {
|
||||
color: #03c2ec;
|
||||
font-family: PingFangSC-Semibold, 'PingFang SC', sans-serif;
|
||||
font-size: clamp(26px, 3vw, 36px);
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
.datav-title {
|
||||
color: #f8fafc;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.analysis-info {
|
||||
background-image: linear-gradient(#d8ae22, #dc9546);
|
||||
color: transparent;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-top: 12px;
|
||||
padding: 0 12px;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
.datav-subtitle {
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.office-content {
|
||||
.datav-content {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: minmax(320px, 0.95fr) minmax(0, 2.1fr);
|
||||
margin-top: 20px;
|
||||
gap: 16px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.office-content__left,
|
||||
.office-content__right {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.col-info {
|
||||
background: rgba(24, 28, 65, 0.96);
|
||||
border: 1px solid rgba(86, 114, 195, 0.28);
|
||||
border-radius: 20px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
.panel {
|
||||
background: rgba(30, 41, 59, 0.82);
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #03c2ec;
|
||||
font-size: clamp(22px, 2vw, 30px);
|
||||
font-weight: 600;
|
||||
padding: 22px 24px 12px;
|
||||
.filter-panel {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.content {
|
||||
color: #fff;
|
||||
padding: 0 24px 24px;
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
background: #18212f;
|
||||
border: 1px solid rgba(45, 212, 191, 0.22);
|
||||
border-radius: 8px;
|
||||
min-height: 92px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.summary-card__label {
|
||||
color: #cbd5e1;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.summary-card__value {
|
||||
color: #f8fafc;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.dashboard-panel {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.number-cell {
|
||||
color: #e2e8f0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.legacy-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: minmax(300px, 1fr) minmax(300px, 1fr);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
color: #2dd4bf;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
padding: 16px 16px 8px;
|
||||
}
|
||||
|
||||
.panel :deep(.ant-table) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.panel :deep(.ant-table-cell) {
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.panel :deep(.ant-table-thead > tr > th) {
|
||||
background: #1f2937;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.panel :deep(.ant-empty-description) {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.office-content {
|
||||
grid-template-columns: minmax(280px, 0.9fr) minmax(0, 1.7fr);
|
||||
.summary-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
@media (max-width: 768px) {
|
||||
.app-container-datav {
|
||||
border-radius: 16px;
|
||||
min-height: calc(100vh - 88px);
|
||||
}
|
||||
|
||||
.office-efficiency-index {
|
||||
padding: 16px;
|
||||
.datav-shell {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.office-content {
|
||||
.datav-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.datav-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.summary-grid,
|
||||
.legacy-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 18px 18px 12px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 18px 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user