初始化2
This commit is contained in:
699
app/pages/developer/publish.vue
Normal file
699
app/pages/developer/publish.vue
Normal file
@@ -0,0 +1,699 @@
|
||||
<template>
|
||||
<div class="dev-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">🚀 发布管理</h2>
|
||||
<p class="page-desc">管理应用的上架、审核状态和版本发布</p>
|
||||
</div>
|
||||
<a-button type="primary" @click="showPublishModal = true">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
提交上架申请
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 状态统计卡片 -->
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="12" :md="6" v-for="stat in publishStats" :key="stat.key">
|
||||
<div class="stat-card" :class="stat.color">
|
||||
<div class="stat-icon">{{ stat.icon }}</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 应用发布列表 -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">📦 我的应用发布</span>
|
||||
<a-space>
|
||||
<a-select v-model:value="filterStatus" style="width: 140px" placeholder="全部状态" @change="loadApps">
|
||||
<a-select-option value="">全部状态</a-select-option>
|
||||
<a-select-option value="developing">开发中</a-select-option>
|
||||
<a-select-option value="pending_review">待审核</a-select-option>
|
||||
<a-select-option value="published">已上架</a-select-option>
|
||||
<a-select-option value="rejected">审核未通过</a-select-option>
|
||||
<a-select-option value="deprecated">已下架</a-select-option>
|
||||
</a-select>
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索应用"
|
||||
style="width: 200px"
|
||||
@search="loadApps"
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="filteredApps"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="productId"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 应用信息列 -->
|
||||
<template v-if="column.key === 'appInfo'">
|
||||
<div class="app-info-cell">
|
||||
<img v-if="record.icon" :src="record.icon" class="app-icon" />
|
||||
<div v-else class="app-icon-placeholder" :style="{ background: iconBgColor(record.productName) }">
|
||||
{{ (record.productName || 'A').charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
<div class="app-info-text">
|
||||
<div class="app-name">{{ record.productName }}</div>
|
||||
<div class="app-code">{{ record.productCode }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'publishStatus'">
|
||||
<a-tag :color="statusColor(record.publishStatus)">
|
||||
{{ statusText(record.publishStatus) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'price'">
|
||||
<div class="price-cell">
|
||||
<span v-if="record.priceType === 'free' || !record.priceType" class="price-free">免费</span>
|
||||
<span v-else class="price-paid">¥{{ ((record.price || 0) / 100).toFixed(2) }}</span>
|
||||
<span class="price-type">{{ priceTypeText(record.priceType) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'stats'">
|
||||
<div class="stats-cell">
|
||||
<span>安装 {{ record.installs || 0 }}</span>
|
||||
<span>评分 {{ record.rating || '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<!-- 开发中:提交审核 -->
|
||||
<a-button
|
||||
v-if="!record.publishStatus || record.publishStatus === 'developing'"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleSubmitReview(record)"
|
||||
>
|
||||
提交审核
|
||||
</a-button>
|
||||
<!-- 待审核:撤回申请 -->
|
||||
<a-popconfirm
|
||||
v-if="record.publishStatus === 'pending_review'"
|
||||
title="确认撤回审核申请?"
|
||||
@confirm="handleWithdraw(record)"
|
||||
>
|
||||
<a-button size="small">撤回申请</a-button>
|
||||
</a-popconfirm>
|
||||
<!-- 已上架:下架 -->
|
||||
<a-popconfirm
|
||||
v-if="record.publishStatus === 'published'"
|
||||
title="确认下架此应用?"
|
||||
ok-text="下架"
|
||||
ok-type="danger"
|
||||
@confirm="handleUnpublish(record)"
|
||||
>
|
||||
<a-button danger size="small">下架</a-button>
|
||||
</a-popconfirm>
|
||||
<!-- 审核未通过:查看原因 + 重新提交 -->
|
||||
<template v-if="record.publishStatus === 'rejected'">
|
||||
<a-button type="link" size="small" @click="handleViewReason(record)">查看原因</a-button>
|
||||
<a-button type="primary" size="small" @click="handleSubmitReview(record)">重新提交</a-button>
|
||||
</template>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 审核记录 -->
|
||||
<div class="panel mt-4">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">📋 审核记录</span>
|
||||
<a-button type="link" size="small" @click="loadReviewLogs" :loading="logsLoading">刷新</a-button>
|
||||
</div>
|
||||
<a-spin :spinning="logsLoading">
|
||||
<a-timeline class="timeline-content" v-if="reviewLogs.length > 0">
|
||||
<a-timeline-item v-for="record in reviewLogs" :key="record.productId" :color="record.color">
|
||||
<div class="timeline-item-content">
|
||||
<div class="timeline-title">{{ record.title }}</div>
|
||||
<div class="timeline-desc">{{ record.desc }}</div>
|
||||
<div class="timeline-time">{{ record.time }}</div>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
<a-empty v-else description="暂无审核记录" class="py-8" />
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<!-- 提交上架申请弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showPublishModal"
|
||||
title="提交上架申请"
|
||||
width="600px"
|
||||
:confirm-loading="submitLoading"
|
||||
@ok="handlePublishSubmit"
|
||||
@cancel="resetPublishForm"
|
||||
>
|
||||
<a-form :model="publishForm" layout="vertical">
|
||||
<a-form-item label="选择应用" required>
|
||||
<a-select v-model:value="publishForm.productId" placeholder="选择要上架的应用">
|
||||
<a-select-option v-for="app in developingApps" :key="app.productId" :value="app.productId">
|
||||
{{ app.productName }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="定价模式" required>
|
||||
<a-radio-group v-model:value="publishForm.priceType">
|
||||
<a-radio value="free">免费</a-radio>
|
||||
<a-radio value="one_time">一次性付费</a-radio>
|
||||
<a-radio value="subscription">订阅制</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="publishForm.priceType !== 'free'" label="价格(元)" required>
|
||||
<a-input-number v-model:value="publishForm.price" :min="0" :precision="2" style="width: 200px" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="publishForm.priceType === 'subscription'" label="订阅周期" required>
|
||||
<a-radio-group v-model:value="publishForm.subscriptionPeriod">
|
||||
<a-radio value="month">按月</a-radio>
|
||||
<a-radio value="year">按年</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="应用简介" required>
|
||||
<a-textarea v-model:value="publishForm.description" :rows="3" placeholder="简要描述应用功能和特点" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="详细说明">
|
||||
<a-textarea v-model:value="publishForm.content" :rows="5" placeholder="详细介绍应用功能、使用场景、技术架构等" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 查看审核原因弹窗 -->
|
||||
<a-modal v-model:open="showReasonModal" title="审核未通过原因" :footer="null">
|
||||
<a-alert type="error" :message="rejectReason" show-icon />
|
||||
<div class="mt-4">
|
||||
<p class="text-gray-600">请根据以上原因修改后重新提交审核。</p>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
pageAppProduct,
|
||||
updateAppProduct,
|
||||
submitReview,
|
||||
withdrawPublishReview,
|
||||
unpublishAppProduct,
|
||||
} from '@/api/app/appProduct'
|
||||
import type { AppProduct } from '@/api/app/appProduct/model'
|
||||
|
||||
definePageMeta({ layout: 'developer' })
|
||||
useHead({ title: '发布管理 - 开发者中心' })
|
||||
|
||||
const userId = import.meta.client ? localStorage.getItem('UserId') : null
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const logsLoading = ref(false)
|
||||
const apps = ref<AppProduct[]>([])
|
||||
|
||||
// 筛选
|
||||
const filterStatus = ref('')
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
})
|
||||
|
||||
// 发布统计
|
||||
const publishStats = reactive([
|
||||
{ key: 'developing', icon: '🔧', label: '开发中', value: 0, color: 'blue' },
|
||||
{ key: 'pending_review', icon: '⏳', label: '待审核', value: 0, color: 'orange' },
|
||||
{ key: 'published', icon: '✅', label: '已上架', value: 0, color: 'green' },
|
||||
{ key: 'rejected', icon: '❌', label: '未通过', value: 0, color: 'red' },
|
||||
])
|
||||
|
||||
// 审核记录(从已处理的应用中聚合生成)
|
||||
interface ReviewLog {
|
||||
productId?: number
|
||||
title: string
|
||||
desc: string
|
||||
time: string
|
||||
color: string
|
||||
}
|
||||
const reviewLogs = ref<ReviewLog[]>([])
|
||||
|
||||
// 表格列
|
||||
const columns = [
|
||||
{ title: '应用信息', key: 'appInfo', width: 280 },
|
||||
{ title: '发布状态', key: 'publishStatus', width: 120 },
|
||||
{ title: '定价', key: 'price', width: 150 },
|
||||
{ title: '数据统计', key: 'stats', width: 150 },
|
||||
{ title: '操作', key: 'action', width: 220 },
|
||||
]
|
||||
|
||||
// 发布弹窗
|
||||
const showPublishModal = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const publishForm = reactive({
|
||||
productId: undefined as number | undefined,
|
||||
priceType: 'free' as 'free' | 'one_time' | 'subscription',
|
||||
price: 0,
|
||||
subscriptionPeriod: 'month' as 'month' | 'year',
|
||||
description: '',
|
||||
content: '',
|
||||
})
|
||||
|
||||
// 查看原因弹窗
|
||||
const showReasonModal = ref(false)
|
||||
const rejectReason = ref('')
|
||||
|
||||
// 开发中的应用列表(可提交上架)
|
||||
const developingApps = computed(() => {
|
||||
return apps.value.filter(
|
||||
app => !app.publishStatus || app.publishStatus === 'developing' || app.publishStatus === 'rejected'
|
||||
)
|
||||
})
|
||||
|
||||
// 前端筛选(关键词过滤)
|
||||
const filteredApps = computed(() => {
|
||||
if (!searchKeyword.value) return apps.value
|
||||
const kw = searchKeyword.value.toLowerCase()
|
||||
return apps.value.filter(
|
||||
app =>
|
||||
app.productName?.toLowerCase().includes(kw) ||
|
||||
app.productCode?.toLowerCase().includes(kw)
|
||||
)
|
||||
})
|
||||
|
||||
// 加载应用列表
|
||||
async function loadApps() {
|
||||
loading.value = true
|
||||
try {
|
||||
const queryUserId = userId ? Number(userId) : undefined
|
||||
const res = await pageAppProduct({
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
userId: queryUserId,
|
||||
publishStatus: filterStatus.value || undefined,
|
||||
})
|
||||
apps.value = res?.list || []
|
||||
pagination.total = res?.count || 0
|
||||
updateStats()
|
||||
} catch {
|
||||
message.error('加载应用列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载审核记录(从应用列表聚合)
|
||||
function loadReviewLogs() {
|
||||
logsLoading.value = true
|
||||
const logs: ReviewLog[] = []
|
||||
|
||||
for (const app of apps.value) {
|
||||
const name = app.productName || '未命名应用'
|
||||
if (app.publishStatus === 'published' && app.publishTime) {
|
||||
logs.push({
|
||||
productId: app.productId,
|
||||
title: `应用「${name}」审核通过并上架`,
|
||||
desc: '恭喜!您的应用已通过审核并上架到市场',
|
||||
time: app.publishTime,
|
||||
color: 'green',
|
||||
})
|
||||
}
|
||||
if (app.publishStatus === 'rejected' && app.reviewTime) {
|
||||
logs.push({
|
||||
productId: app.productId,
|
||||
title: `应用「${name}」审核未通过`,
|
||||
desc: app.rejectReason || '请查看具体原因后修改重新提交',
|
||||
time: app.reviewTime,
|
||||
color: 'red',
|
||||
})
|
||||
}
|
||||
if (app.publishStatus === 'pending_review' && app.publishTime) {
|
||||
logs.push({
|
||||
productId: app.productId,
|
||||
title: `应用「${name}」已提交审核`,
|
||||
desc: '等待平台审核人员审核,通常 1-3 个工作日',
|
||||
time: app.publishTime,
|
||||
color: 'blue',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间降序
|
||||
logs.sort((a, b) => (a.time < b.time ? 1 : -1))
|
||||
reviewLogs.value = logs
|
||||
logsLoading.value = false
|
||||
}
|
||||
|
||||
// 更新统计数据
|
||||
function updateStats() {
|
||||
publishStats[0].value = apps.value.filter(a => !a.publishStatus || a.publishStatus === 'developing').length
|
||||
publishStats[1].value = apps.value.filter(a => a.publishStatus === 'pending_review').length
|
||||
publishStats[2].value = apps.value.filter(a => a.publishStatus === 'published').length
|
||||
publishStats[3].value = apps.value.filter(a => a.publishStatus === 'rejected').length
|
||||
loadReviewLogs()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
function handleTableChange(pag: any) {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadApps()
|
||||
}
|
||||
|
||||
// 状态文本
|
||||
function statusText(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
developing: '开发中',
|
||||
pending_review: '待审核',
|
||||
published: '已上架',
|
||||
rejected: '审核未通过',
|
||||
deprecated: '已下架',
|
||||
}
|
||||
return map[status || ''] || '开发中'
|
||||
}
|
||||
|
||||
// 状态颜色
|
||||
function statusColor(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
developing: 'default',
|
||||
pending_review: 'orange',
|
||||
published: 'success',
|
||||
rejected: 'error',
|
||||
deprecated: 'default',
|
||||
}
|
||||
return map[status || ''] || 'default'
|
||||
}
|
||||
|
||||
// 价格类型文本
|
||||
function priceTypeText(type?: string) {
|
||||
const map: Record<string, string> = {
|
||||
free: '',
|
||||
one_time: '一次性',
|
||||
subscription: '订阅',
|
||||
}
|
||||
return map[type || ''] ?? ''
|
||||
}
|
||||
|
||||
// 图标背景色
|
||||
const PALETTE = ['#4e6ef2', '#f4a261', '#e76f51', '#2a9d8f', '#e9c46a', '#457b9d']
|
||||
function iconBgColor(name?: string) {
|
||||
if (!name) return PALETTE[0]
|
||||
let h = 0
|
||||
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) & 0xffffffff
|
||||
return PALETTE[Math.abs(h) % PALETTE.length]
|
||||
}
|
||||
|
||||
// 操作:提交审核(打开弹窗)
|
||||
function handleSubmitReview(record: AppProduct) {
|
||||
publishForm.productId = record.productId
|
||||
publishForm.priceType = record.priceType || 'free'
|
||||
publishForm.price = record.price ? record.price / 100 : 0
|
||||
publishForm.description = record.description || ''
|
||||
publishForm.content = record.content || ''
|
||||
showPublishModal.value = true
|
||||
}
|
||||
|
||||
// 操作:撤回审核
|
||||
async function handleWithdraw(record: AppProduct) {
|
||||
try {
|
||||
await withdrawPublishReview(record.productId!)
|
||||
message.success('已撤回审核申请')
|
||||
loadApps()
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '撤回失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 操作:下架
|
||||
async function handleUnpublish(record: AppProduct) {
|
||||
try {
|
||||
await unpublishAppProduct(record.productId!)
|
||||
message.success('已下架')
|
||||
loadApps()
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '下架失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 操作:查看拒绝原因
|
||||
function handleViewReason(record: AppProduct) {
|
||||
rejectReason.value = record.rejectReason || '审核未通过,请联系平台客服了解详情。'
|
||||
showReasonModal.value = true
|
||||
}
|
||||
|
||||
// 提交上架申请
|
||||
async function handlePublishSubmit() {
|
||||
if (!publishForm.productId) {
|
||||
message.error('请选择应用')
|
||||
return
|
||||
}
|
||||
if (!publishForm.description.trim()) {
|
||||
message.error('请填写应用简介')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
// 先更新产品信息(简介、定价等)
|
||||
await updateAppProduct({
|
||||
productId: publishForm.productId,
|
||||
description: publishForm.description,
|
||||
content: publishForm.content,
|
||||
priceType: publishForm.priceType,
|
||||
price: publishForm.priceType !== 'free' ? Math.round(publishForm.price * 100) : 0,
|
||||
subscriptionPeriod: publishForm.priceType === 'subscription' ? publishForm.subscriptionPeriod : undefined,
|
||||
} as Partial<AppProduct>)
|
||||
// 再提交审核
|
||||
await submitReview(publishForm.productId)
|
||||
message.success('上架申请提交成功,等待审核(通常 1-3 个工作日)')
|
||||
showPublishModal.value = false
|
||||
resetPublishForm()
|
||||
loadApps()
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '提交失败,请稍后重试')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetPublishForm() {
|
||||
publishForm.productId = undefined
|
||||
publishForm.priceType = 'free'
|
||||
publishForm.price = 0
|
||||
publishForm.subscriptionPeriod = 'month'
|
||||
publishForm.description = ''
|
||||
publishForm.content = ''
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadApps()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dev-page {
|
||||
min-height: 100%;
|
||||
padding: 20px 24px 28px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.stat-card:hover { transform: translateY(-1px); }
|
||||
|
||||
.stat-card.blue { background: #eff6ff; border-color: #dbeafe; }
|
||||
.stat-card.orange { background: #fff7ed; border-color: #fed7aa; }
|
||||
.stat-card.green { background: #f0fdf4; border-color: #bbf7d0; }
|
||||
.stat-card.red { background: #fff1f2; border-color: #fecdd3; }
|
||||
|
||||
.stat-icon { font-size: 28px; flex-shrink: 0; }
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 面板 */
|
||||
.panel {
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
/* 表格单元格 */
|
||||
.app-info-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.app-icon-placeholder {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-info-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.app-code {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.price-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.price-free {
|
||||
color: #22c55e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.price-paid {
|
||||
color: #f59e0b;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.price-type {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.stats-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
/* 时间线 */
|
||||
.timeline-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.timeline-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.timeline-desc {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.timeline-time {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.mb-6 { margin-bottom: 24px; }
|
||||
.mt-4 { margin-top: 16px; }
|
||||
.py-8 { padding: 32px 0; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user