aa96143754
Apply repo-wide Prettier/lint normalization across admin-web, desktop-client and ops-web: single quotes, no semicolons, trailing commas, consistent line wrapping, and import ordering. Also drop an unused brand-logo import in DesktopShell.vue. No behavior changes — formatting only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
757 lines
23 KiB
Vue
757 lines
23 KiB
Vue
<template>
|
|
<div class="media-ops-page">
|
|
<div class="media-ops-header">
|
|
<div>
|
|
<h2 class="ops-page-title">媒体资源运营</h2>
|
|
<div class="media-ops-header__meta">上游资源售价、客户可见性、投稿余额</div>
|
|
</div>
|
|
<a-button :loading="syncLoading" @click="queueSync">
|
|
<template #icon><ReloadOutlined /></template>
|
|
同步缓存
|
|
</a-button>
|
|
</div>
|
|
|
|
<a-tabs v-model:active-key="activeTab" class="media-ops-tabs">
|
|
<a-tab-pane key="resources" tab="资源与价格">
|
|
<div class="ops-card media-ops-card">
|
|
<div class="ops-toolbar media-ops-toolbar">
|
|
<a-input
|
|
v-model:value="resourceFilter.keyword"
|
|
allow-clear
|
|
placeholder="资源名称"
|
|
style="width: 220px"
|
|
@press-enter="resetResources"
|
|
/>
|
|
<a-input
|
|
v-model:value="resourceFilter.remarkKeyword"
|
|
allow-clear
|
|
placeholder="备注"
|
|
style="width: 220px"
|
|
@press-enter="resetResources"
|
|
/>
|
|
<a-select
|
|
v-model:value="resourceFilter.visibility"
|
|
allow-clear
|
|
placeholder="客户可见性"
|
|
style="width: 150px"
|
|
:options="visibilityOptions"
|
|
@change="resetResources"
|
|
/>
|
|
<a-input
|
|
v-model:value="resourceFilter.channelType"
|
|
allow-clear
|
|
placeholder="频道类型"
|
|
style="width: 150px"
|
|
@press-enter="resetResources"
|
|
/>
|
|
<a-input
|
|
v-model:value="resourceFilter.region"
|
|
allow-clear
|
|
placeholder="地区"
|
|
style="width: 130px"
|
|
@press-enter="resetResources"
|
|
/>
|
|
<a-input
|
|
v-model:value="resourceFilter.minPrice"
|
|
allow-clear
|
|
placeholder="最低售价"
|
|
style="width: 120px"
|
|
@press-enter="resetResources"
|
|
/>
|
|
<a-input
|
|
v-model:value="resourceFilter.maxPrice"
|
|
allow-clear
|
|
placeholder="最高售价"
|
|
style="width: 120px"
|
|
@press-enter="resetResources"
|
|
/>
|
|
<a-button type="primary" @click="resetResources">查询</a-button>
|
|
<a-button @click="clearResourceFilters">重置</a-button>
|
|
</div>
|
|
|
|
<a-table
|
|
:columns="resourceColumns"
|
|
:data-source="resourceRows"
|
|
:loading="resourceLoading"
|
|
:pagination="resourcePagination"
|
|
row-key="id"
|
|
:scroll="{ x: 1680 }"
|
|
@change="onResourceTableChange"
|
|
>
|
|
<template #bodyCell="{ column, record }">
|
|
<template v-if="column.key === 'name'">
|
|
<div class="resource-main">
|
|
<a
|
|
v-if="isValidUrl(record.resource_url)"
|
|
:href="record.resource_url || undefined"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
class="resource-link"
|
|
>
|
|
{{ record.name }}
|
|
</a>
|
|
<strong v-else>{{ record.name }}</strong>
|
|
<div class="resource-sub">
|
|
<code>{{ record.supplier_resource_id }}</code>
|
|
<a-tag :color="statusColor(record.status)">
|
|
{{ statusLabel(record.status) }}
|
|
</a-tag>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template v-else-if="column.key === 'price'">
|
|
<div class="price-stack">
|
|
<strong>{{ formatSellPrice(record.sell_price_cents) }}</strong>
|
|
<span>成本 {{ formatMoney(record.cost_price_cents) }}</span>
|
|
</div>
|
|
</template>
|
|
<template v-else-if="column.key === 'margin'">
|
|
<span
|
|
:class="
|
|
record.sell_price_cents <= record.cost_price_cents ? 'margin-risk' : 'margin-ok'
|
|
"
|
|
>
|
|
{{ formatMoney(record.sell_price_cents - record.cost_price_cents) }}
|
|
</span>
|
|
</template>
|
|
<template v-else-if="column.key === 'baidu_weight'">
|
|
<a-tag color="blue">百度 {{ record.baidu_weight ?? 0 }}</a-tag>
|
|
</template>
|
|
<template v-else-if="column.key === 'customer_visible'">
|
|
<a-switch
|
|
:checked="record.customer_visible"
|
|
checked-children="显示"
|
|
un-checked-children="隐藏"
|
|
:loading="visibilityLoadingId === record.id"
|
|
@change="
|
|
(checked: boolean | string | number) =>
|
|
updateVisibility(record, Boolean(checked))
|
|
"
|
|
/>
|
|
</template>
|
|
<template v-else-if="column.key === 'remark'">
|
|
<a-tooltip :title="displayValue(record.resource_remark)">
|
|
<span class="remark-cell">{{ displayValue(record.resource_remark) }}</span>
|
|
</a-tooltip>
|
|
</template>
|
|
<template v-else-if="column.key === 'last_synced_at'">
|
|
{{ formatDate(record.last_synced_at) }}
|
|
</template>
|
|
<template v-else-if="column.key === 'actions'">
|
|
<div class="table-actions-row">
|
|
<a-tooltip title="调整售价">
|
|
<a-button
|
|
type="text"
|
|
class="action-btn action-edit"
|
|
@click="openPriceModal(record)"
|
|
>
|
|
<EditOutlined />
|
|
</a-button>
|
|
</a-tooltip>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
</a-table>
|
|
</div>
|
|
</a-tab-pane>
|
|
|
|
<a-tab-pane key="wallets" tab="客户余额">
|
|
<div class="ops-card media-ops-card">
|
|
<div class="ops-toolbar media-ops-toolbar">
|
|
<a-input
|
|
v-model:value="walletFilter.keyword"
|
|
allow-clear
|
|
placeholder="客户 / 手机 / 租户"
|
|
style="width: 260px"
|
|
@press-enter="resetWallets"
|
|
/>
|
|
<a-input
|
|
v-model:value="walletFilter.tenantId"
|
|
allow-clear
|
|
placeholder="租户 ID"
|
|
style="width: 130px"
|
|
@press-enter="resetWallets"
|
|
/>
|
|
<a-button type="primary" @click="resetWallets">查询</a-button>
|
|
<a-button @click="clearWalletFilters">重置</a-button>
|
|
</div>
|
|
|
|
<a-table
|
|
:columns="walletColumns"
|
|
:data-source="walletRows"
|
|
:loading="walletLoading"
|
|
:pagination="walletPagination"
|
|
row-key="wallet_key"
|
|
:scroll="{ x: 1100 }"
|
|
@change="onWalletTableChange"
|
|
>
|
|
<template #bodyCell="{ column, record }">
|
|
<template v-if="column.key === 'user'">
|
|
<div class="resource-main">
|
|
<strong>
|
|
{{ record.user_name || record.user_phone || `用户 ${record.user_id}` }}
|
|
</strong>
|
|
<div class="resource-sub">
|
|
<code>U{{ record.user_id }}</code>
|
|
<span>{{ record.user_phone || '未绑定手机' }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template v-else-if="column.key === 'tenant'">
|
|
<div>{{ record.tenant_name || `租户 ${record.tenant_id}` }}</div>
|
|
<div class="muted">T{{ record.tenant_id }}</div>
|
|
</template>
|
|
<template v-else-if="column.key === 'balance_cents'">
|
|
<strong class="wallet-balance">{{ formatMoney(record.balance_cents) }}</strong>
|
|
</template>
|
|
<template v-else-if="column.key === 'ledger'">
|
|
<div>{{ record.ledger_entries }} 条</div>
|
|
<div class="muted">
|
|
{{ record.last_ledger_at ? formatDate(record.last_ledger_at) : '暂无账单' }}
|
|
</div>
|
|
</template>
|
|
<template v-else-if="column.key === 'updated_at'">
|
|
{{ formatDate(record.updated_at) }}
|
|
</template>
|
|
<template v-else-if="column.key === 'actions'">
|
|
<a-button type="link" @click="openWalletModal(record)">调整余额</a-button>
|
|
</template>
|
|
</template>
|
|
</a-table>
|
|
</div>
|
|
</a-tab-pane>
|
|
</a-tabs>
|
|
|
|
<a-modal
|
|
v-model:open="priceModalOpen"
|
|
title="调整媒体售价"
|
|
:confirm-loading="priceSaving"
|
|
@ok="submitPrice"
|
|
@cancel="priceTarget = null"
|
|
>
|
|
<div v-if="priceTarget" class="edit-stack">
|
|
<a-descriptions :column="1" size="small" bordered>
|
|
<a-descriptions-item label="资源">{{ priceTarget.name }}</a-descriptions-item>
|
|
<a-descriptions-item label="成本">
|
|
{{ formatMoney(priceTarget.cost_price_cents) }}
|
|
</a-descriptions-item>
|
|
<a-descriptions-item label="当前售价">
|
|
{{ formatSellPrice(priceTarget.sell_price_cents) }}
|
|
</a-descriptions-item>
|
|
</a-descriptions>
|
|
<a-form layout="vertical">
|
|
<a-form-item label="平台售价">
|
|
<a-input v-model:value="priceForm.yuan" prefix="¥" placeholder="不能低于上游成本价" />
|
|
</a-form-item>
|
|
</a-form>
|
|
</div>
|
|
</a-modal>
|
|
|
|
<a-modal
|
|
v-model:open="walletModalOpen"
|
|
title="手动调整客户媒体余额"
|
|
:confirm-loading="walletSaving"
|
|
@ok="submitWalletAdjustment"
|
|
@cancel="walletTarget = null"
|
|
>
|
|
<div v-if="walletTarget" class="edit-stack">
|
|
<a-descriptions :column="1" size="small" bordered>
|
|
<a-descriptions-item label="客户">
|
|
{{ walletTarget.user_name || walletTarget.user_phone || walletTarget.user_id }}
|
|
</a-descriptions-item>
|
|
<a-descriptions-item label="租户">
|
|
{{ walletTarget.tenant_name || walletTarget.tenant_id }}
|
|
</a-descriptions-item>
|
|
<a-descriptions-item label="当前余额">
|
|
{{ formatMoney(walletTarget.balance_cents) }}
|
|
</a-descriptions-item>
|
|
</a-descriptions>
|
|
<a-form layout="vertical">
|
|
<a-form-item label="调整金额">
|
|
<a-input v-model:value="walletForm.yuan" prefix="¥" placeholder="正数充值,负数扣减" />
|
|
</a-form-item>
|
|
<a-form-item label="备注">
|
|
<a-textarea
|
|
v-model:value="walletForm.note"
|
|
:rows="3"
|
|
placeholder="账单备注,客户可见"
|
|
/>
|
|
</a-form-item>
|
|
</a-form>
|
|
</div>
|
|
</a-modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { EditOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
|
import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue'
|
|
import { message } from 'ant-design-vue'
|
|
import dayjs from 'dayjs'
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
|
|
import { showOpsError } from '@/lib/errors'
|
|
import {
|
|
opsMediaSupplyApi,
|
|
type OpsMediaSupplyResource,
|
|
type OpsMediaSupplyWallet,
|
|
} from '@/lib/media-supply'
|
|
|
|
interface WalletRow extends OpsMediaSupplyWallet {
|
|
wallet_key: string
|
|
}
|
|
|
|
const MODEL_ID_AUTHORITY_NEWS = 1
|
|
const PAGE_SIZE = 20
|
|
|
|
const activeTab = ref('resources')
|
|
const resourcePage = ref(1)
|
|
const resourcePageSize = ref(PAGE_SIZE)
|
|
const resourceLoading = ref(false)
|
|
const resourceTotal = ref(0)
|
|
const resourceRows = ref<OpsMediaSupplyResource[]>([])
|
|
const visibilityLoadingId = ref<number | null>(null)
|
|
const syncLoading = ref(false)
|
|
const priceModalOpen = ref(false)
|
|
const priceSaving = ref(false)
|
|
const priceTarget = ref<OpsMediaSupplyResource | null>(null)
|
|
const priceForm = reactive({
|
|
yuan: '',
|
|
})
|
|
|
|
const walletPage = ref(1)
|
|
const walletPageSize = ref(PAGE_SIZE)
|
|
const walletLoading = ref(false)
|
|
const walletTotal = ref(0)
|
|
const walletRows = ref<WalletRow[]>([])
|
|
const walletModalOpen = ref(false)
|
|
const walletSaving = ref(false)
|
|
const walletTarget = ref<WalletRow | null>(null)
|
|
const walletForm = reactive({
|
|
yuan: '',
|
|
note: '',
|
|
})
|
|
|
|
const resourceFilter = reactive({
|
|
keyword: '',
|
|
remarkKeyword: '',
|
|
visibility: undefined as string | undefined,
|
|
channelType: '',
|
|
region: '',
|
|
minPrice: '',
|
|
maxPrice: '',
|
|
})
|
|
|
|
const walletFilter = reactive({
|
|
keyword: '',
|
|
tenantId: '',
|
|
})
|
|
|
|
const visibilityOptions = [
|
|
{ label: '客户可见', value: 'visible' },
|
|
{ label: '客户隐藏', value: 'hidden' },
|
|
]
|
|
|
|
const resourceColumns: TableColumnsType<OpsMediaSupplyResource> = [
|
|
{ title: '媒体名称', key: 'name', width: 320, fixed: 'left' },
|
|
{ title: '售价 / 成本', key: 'price', width: 150, align: 'right' },
|
|
{ title: '毛利', key: 'margin', width: 120, align: 'right' },
|
|
{ title: '频道', dataIndex: 'channel_type', key: 'channel_type', width: 120 },
|
|
{ title: '地区', dataIndex: 'region', key: 'region', width: 110 },
|
|
{ title: '收录', dataIndex: 'inclusion_effect', key: 'inclusion_effect', width: 140 },
|
|
{ title: '权重', key: 'baidu_weight', width: 110, align: 'center' },
|
|
{ title: '链接', dataIndex: 'link_type', key: 'link_type', width: 110 },
|
|
{ title: '出稿', dataIndex: 'publish_rate', key: 'publish_rate', width: 110 },
|
|
{ title: '速度', dataIndex: 'delivery_speed', key: 'delivery_speed', width: 140 },
|
|
{ title: '备注', key: 'remark', width: 320 },
|
|
{ title: '客户可见', key: 'customer_visible', width: 130, align: 'center' },
|
|
{ title: '同步时间', key: 'last_synced_at', width: 170 },
|
|
{ title: '操作', key: 'actions', width: 90, fixed: 'right', align: 'center' },
|
|
]
|
|
|
|
const walletColumns: TableColumnsType<WalletRow> = [
|
|
{ title: '客户', key: 'user', width: 260, fixed: 'left' },
|
|
{ title: '租户', key: 'tenant', width: 220 },
|
|
{ title: '余额', key: 'balance_cents', width: 160, align: 'right' },
|
|
{ title: '账单', key: 'ledger', width: 180 },
|
|
{ title: '更新时间', key: 'updated_at', width: 180 },
|
|
{ title: '操作', key: 'actions', width: 130, fixed: 'right', align: 'center' },
|
|
]
|
|
|
|
const resourcePagination = computed<TablePaginationConfig>(() => ({
|
|
current: resourcePage.value,
|
|
pageSize: resourcePageSize.value,
|
|
total: resourceTotal.value,
|
|
showSizeChanger: true,
|
|
showTotal: (total) => `共 ${total} 条`,
|
|
}))
|
|
|
|
const walletPagination = computed<TablePaginationConfig>(() => ({
|
|
current: walletPage.value,
|
|
pageSize: walletPageSize.value,
|
|
total: walletTotal.value,
|
|
showSizeChanger: true,
|
|
showTotal: (total) => `共 ${total} 条`,
|
|
}))
|
|
|
|
watch(activeTab, (key) => {
|
|
if (key === 'wallets' && walletRows.value.length === 0) {
|
|
void loadWallets()
|
|
}
|
|
})
|
|
|
|
onMounted(() => {
|
|
void loadResources()
|
|
})
|
|
|
|
async function loadResources(): Promise<void> {
|
|
resourceLoading.value = true
|
|
try {
|
|
const result = await opsMediaSupplyApi.listResources({
|
|
model_id: MODEL_ID_AUTHORITY_NEWS,
|
|
keyword: resourceFilter.keyword.trim() || undefined,
|
|
remark_keyword: resourceFilter.remarkKeyword.trim() || undefined,
|
|
visibility: resourceFilter.visibility,
|
|
channel_type: resourceFilter.channelType.trim() || undefined,
|
|
region: resourceFilter.region.trim() || undefined,
|
|
min_price_cents: yuanToCentsOptional(resourceFilter.minPrice),
|
|
max_price_cents: yuanToCentsOptional(resourceFilter.maxPrice),
|
|
page: resourcePage.value,
|
|
page_size: resourcePageSize.value,
|
|
})
|
|
resourceRows.value = result.items
|
|
resourceTotal.value = result.total
|
|
} catch (error) {
|
|
showOpsError(error)
|
|
} finally {
|
|
resourceLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadWallets(): Promise<void> {
|
|
walletLoading.value = true
|
|
try {
|
|
const result = await opsMediaSupplyApi.listWallets({
|
|
keyword: walletFilter.keyword.trim() || undefined,
|
|
tenant_id: parsePositiveNumber(walletFilter.tenantId),
|
|
page: walletPage.value,
|
|
page_size: walletPageSize.value,
|
|
})
|
|
walletRows.value = result.items.map((item) => ({
|
|
...item,
|
|
wallet_key: `${item.tenant_id}:${item.user_id}`,
|
|
}))
|
|
walletTotal.value = result.total
|
|
} catch (error) {
|
|
showOpsError(error)
|
|
} finally {
|
|
walletLoading.value = false
|
|
}
|
|
}
|
|
|
|
function resetResources(): void {
|
|
resourcePage.value = 1
|
|
void loadResources()
|
|
}
|
|
|
|
function clearResourceFilters(): void {
|
|
resourceFilter.keyword = ''
|
|
resourceFilter.remarkKeyword = ''
|
|
resourceFilter.visibility = undefined
|
|
resourceFilter.channelType = ''
|
|
resourceFilter.region = ''
|
|
resourceFilter.minPrice = ''
|
|
resourceFilter.maxPrice = ''
|
|
resetResources()
|
|
}
|
|
|
|
function resetWallets(): void {
|
|
walletPage.value = 1
|
|
void loadWallets()
|
|
}
|
|
|
|
function clearWalletFilters(): void {
|
|
walletFilter.keyword = ''
|
|
walletFilter.tenantId = ''
|
|
resetWallets()
|
|
}
|
|
|
|
function onResourceTableChange(pagination: TablePaginationConfig): void {
|
|
resourcePage.value = pagination.current ?? 1
|
|
resourcePageSize.value = pagination.pageSize ?? PAGE_SIZE
|
|
void loadResources()
|
|
}
|
|
|
|
function onWalletTableChange(pagination: TablePaginationConfig): void {
|
|
walletPage.value = pagination.current ?? 1
|
|
walletPageSize.value = pagination.pageSize ?? PAGE_SIZE
|
|
void loadWallets()
|
|
}
|
|
|
|
async function queueSync(): Promise<void> {
|
|
syncLoading.value = true
|
|
try {
|
|
await opsMediaSupplyApi.queueSync(MODEL_ID_AUTHORITY_NEWS)
|
|
message.success('调度任务已提交')
|
|
} catch (error) {
|
|
showOpsError(error)
|
|
} finally {
|
|
syncLoading.value = false
|
|
}
|
|
}
|
|
|
|
function openPriceModal(row: OpsMediaSupplyResource): void {
|
|
priceTarget.value = row
|
|
priceForm.yuan = centsToYuan(row.sell_price_cents)
|
|
priceModalOpen.value = true
|
|
}
|
|
|
|
async function submitPrice(): Promise<void> {
|
|
if (!priceTarget.value) return
|
|
const cents = roundUpToYuanCents(yuanToCents(priceForm.yuan))
|
|
if (cents <= 0) {
|
|
message.warning('请输入有效售价')
|
|
return
|
|
}
|
|
if (cents < priceTarget.value.cost_price_cents) {
|
|
message.warning('售价不能低于成本价')
|
|
return
|
|
}
|
|
priceSaving.value = true
|
|
try {
|
|
await opsMediaSupplyApi.setPrice(priceTarget.value.id, {
|
|
price_type: 'price',
|
|
sell_price_cents: cents,
|
|
enabled: true,
|
|
})
|
|
message.success('售价已更新')
|
|
priceModalOpen.value = false
|
|
priceTarget.value = null
|
|
await loadResources()
|
|
} catch (error) {
|
|
showOpsError(error)
|
|
} finally {
|
|
priceSaving.value = false
|
|
}
|
|
}
|
|
|
|
async function updateVisibility(row: OpsMediaSupplyResource, checked: boolean): Promise<void> {
|
|
visibilityLoadingId.value = row.id
|
|
try {
|
|
await opsMediaSupplyApi.setVisibility(row.id, checked)
|
|
row.customer_visible = checked
|
|
message.success(checked ? '已在客户后台显示' : '已从客户后台隐藏')
|
|
} catch (error) {
|
|
showOpsError(error)
|
|
} finally {
|
|
visibilityLoadingId.value = null
|
|
}
|
|
}
|
|
|
|
function openWalletModal(row: WalletRow): void {
|
|
walletTarget.value = row
|
|
walletForm.yuan = ''
|
|
walletForm.note = ''
|
|
walletModalOpen.value = true
|
|
}
|
|
|
|
async function submitWalletAdjustment(): Promise<void> {
|
|
if (!walletTarget.value) return
|
|
const cents = yuanToCents(walletForm.yuan)
|
|
if (cents === 0) {
|
|
message.warning('请输入调整金额')
|
|
return
|
|
}
|
|
if (walletTarget.value.balance_cents + cents < 0) {
|
|
message.warning('扣减后余额不能为负')
|
|
return
|
|
}
|
|
walletSaving.value = true
|
|
try {
|
|
await opsMediaSupplyApi.adjustWallet({
|
|
tenant_id: walletTarget.value.tenant_id,
|
|
user_id: walletTarget.value.user_id,
|
|
delta_cents: cents,
|
|
note: walletForm.note.trim() || undefined,
|
|
})
|
|
message.success('余额已调整')
|
|
walletModalOpen.value = false
|
|
walletTarget.value = null
|
|
await loadWallets()
|
|
} catch (error) {
|
|
showOpsError(error)
|
|
} finally {
|
|
walletSaving.value = false
|
|
}
|
|
}
|
|
|
|
function formatMoney(cents?: number | null): string {
|
|
if (typeof cents !== 'number' || !Number.isFinite(cents)) return '--'
|
|
return `¥${(cents / 100).toFixed(2)}`
|
|
}
|
|
|
|
function formatSellPrice(cents?: number | null): string {
|
|
if (typeof cents !== 'number' || !Number.isFinite(cents)) return '--'
|
|
return `¥${Math.ceil(cents / 100)}`
|
|
}
|
|
|
|
function centsToYuan(cents: number): string {
|
|
return (cents / 100).toFixed(2)
|
|
}
|
|
|
|
function yuanToCents(value: string): number {
|
|
const numeric = Number(value)
|
|
if (!Number.isFinite(numeric)) return 0
|
|
return Math.round(numeric * 100)
|
|
}
|
|
|
|
function roundUpToYuanCents(cents: number): number {
|
|
return cents > 0 ? Math.ceil(cents / 100) * 100 : 0
|
|
}
|
|
|
|
function yuanToCentsOptional(value: string): number | undefined {
|
|
const cents = yuanToCents(value)
|
|
return cents > 0 ? cents : undefined
|
|
}
|
|
|
|
function parsePositiveNumber(value: string): number | undefined {
|
|
const numeric = Number(value)
|
|
return Number.isFinite(numeric) && numeric > 0 ? numeric : undefined
|
|
}
|
|
|
|
function displayValue(value?: string | null): string {
|
|
return value?.trim() || '--'
|
|
}
|
|
|
|
function formatDate(value?: string | null): string {
|
|
if (!value) return '--'
|
|
return dayjs(value).isValid() ? dayjs(value).format('YYYY-MM-DD HH:mm') : '--'
|
|
}
|
|
|
|
function isValidUrl(value?: string | null): boolean {
|
|
return /^https?:\/\//i.test(String(value ?? '').trim())
|
|
}
|
|
|
|
function statusLabel(status: string): string {
|
|
if (status === 'normal' || status === 'enabled') return '可投'
|
|
if (status === 'disabled') return '不可投'
|
|
return status || '--'
|
|
}
|
|
|
|
function statusColor(status: string): string {
|
|
if (status === 'normal' || status === 'enabled') return 'green'
|
|
if (status === 'disabled') return 'default'
|
|
return 'blue'
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.media-ops-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.media-ops-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
}
|
|
|
|
.media-ops-header .ops-page-title {
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.media-ops-header__meta {
|
|
color: #64748b;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.media-ops-tabs :deep(.ant-tabs-nav) {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.media-ops-card {
|
|
padding: 16px;
|
|
}
|
|
|
|
.media-ops-toolbar {
|
|
gap: 10px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.resource-main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.resource-main strong,
|
|
.resource-link,
|
|
.remark-cell {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.resource-link {
|
|
color: #1677ff;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.resource-sub {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: #64748b;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.price-stack {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
gap: 2px;
|
|
}
|
|
|
|
.price-stack span,
|
|
.muted {
|
|
color: #64748b;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.margin-ok {
|
|
color: #15803d;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.margin-risk {
|
|
color: #b91c1c;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.remark-cell {
|
|
display: block;
|
|
max-width: 300px;
|
|
}
|
|
|
|
.wallet-balance {
|
|
color: #0f766e;
|
|
}
|
|
|
|
.edit-stack {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.media-ops-header {
|
|
align-items: flex-start;
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
</style>
|