- 为控制台首页添加页面标题动态设置 - 为应用中心页面添加页面标题动态设置 - 修改控制台布局,实现动态浏览器标签页标题更新 - 新增“天天系统”ERP管理平台主页,包含侧边栏导航、顶部栏及数据概览模块 - 实现主页搜索框、通知、语言和用户信息区域交互 - 添加欢迎区、快捷入口、最近使用应用列表及应用详情抽屉功能 - 支持小程序扫码弹窗展示和应用类型图标及颜色区分 - 优化页面样式,支持响应式布局及交互效果 - 更新Nuxt国际化重定向路径片段标识符以兼容新版本
787 lines
21 KiB
Vue
787 lines
21 KiB
Vue
<template>
|
||
<div>
|
||
<!-- 页面头部 -->
|
||
<div class="page-header">
|
||
<div>
|
||
<h2 class="page-title">应用中心</h2>
|
||
<p class="page-desc">管理你订阅的所有应用,快速进入后台</p>
|
||
</div>
|
||
<a-space>
|
||
<a-button @click="navigateTo('/market')">
|
||
<template #icon><ShopOutlined /></template>
|
||
浏览应用商店
|
||
</a-button>
|
||
</a-space>
|
||
</div>
|
||
|
||
<!-- Tab 切换 -->
|
||
<a-tabs v-model:activeKey="activeTab" class="apps-tabs" @change="handleTabChange">
|
||
<a-tab-pane key="my-apps" tab="我的应用">
|
||
<AppsCenter :user-id="userId" />
|
||
</a-tab-pane>
|
||
|
||
<a-tab-pane key="purchased" tab="已购应用">
|
||
<div class="purchased-section">
|
||
<!-- 筛选栏 -->
|
||
<div class="filter-bar">
|
||
<a-input-search
|
||
v-model:value="purchasedSearch"
|
||
placeholder="搜索已购应用"
|
||
style="width: 300px"
|
||
@search="loadPurchasedApps"
|
||
/>
|
||
<a-select v-model:value="purchasedFilter" style="width: 140px" placeholder="全部状态">
|
||
<a-select-option value="">全部状态</a-select-option>
|
||
<a-select-option value="active">生效中</a-select-option>
|
||
<a-select-option value="expired">已过期</a-select-option>
|
||
<a-select-option value="cancelled">已取消</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<!-- 已购应用列表 -->
|
||
<a-table
|
||
:columns="purchasedColumns"
|
||
:data-source="filteredPurchasedApps"
|
||
:loading="purchasedLoading"
|
||
row-key="id"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'appInfo'">
|
||
<div class="app-info-cell">
|
||
<img v-if="record.appIcon" :src="record.appIcon" class="app-icon" />
|
||
<div v-else class="app-icon-placeholder" :style="{ background: iconBgColor(record.appName) }">
|
||
{{ (record.appName || 'A').charAt(0).toUpperCase() }}
|
||
</div>
|
||
<div class="app-info-text">
|
||
<div class="app-name">{{ record.appName }}</div>
|
||
<div class="app-developer">开发者:{{ record.developerName }}</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-if="column.key === 'subscription'">
|
||
<div class="subscription-cell">
|
||
<div class="price-type">
|
||
<span v-if="record.priceType === 'free'" class="text-green-500">免费</span>
|
||
<span v-else class="text-orange-500">¥{{ (record.price || 0) / 100 }}</span>
|
||
<a-tag size="small" class="ml-2">{{ priceTypeText(record.priceType) }}</a-tag>
|
||
</div>
|
||
<div class="expire-time" v-if="record.endTime">
|
||
到期:{{ record.endTime }}
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-if="column.key === 'status'">
|
||
<a-tag :color="subscriptionStatusColor(record.status)">
|
||
{{ subscriptionStatusText(record.status) }}
|
||
</a-tag>
|
||
<a-switch
|
||
v-if="record.status === 'active'"
|
||
v-model:checked="record.enabled"
|
||
size="small"
|
||
class="ml-2"
|
||
@change="(val) => handleToggleApp(record, val)"
|
||
/>
|
||
</template>
|
||
|
||
<template v-if="column.key === 'action'">
|
||
<a-space>
|
||
<a-button type="primary" size="small" @click="handleEnterApp(record)">
|
||
进入应用
|
||
</a-button>
|
||
<a-button size="small" @click="handleConfig(record)">
|
||
配置
|
||
</a-button>
|
||
<a-dropdown>
|
||
<a-button size="small">
|
||
<MoreOutlined />
|
||
</a-button>
|
||
<template #overlay>
|
||
<a-menu>
|
||
<a-menu-item @click="handleRenew(record)">续费</a-menu-item>
|
||
<a-menu-item @click="handleViewDetail(record)">查看详情</a-menu-item>
|
||
<a-menu-divider />
|
||
<a-menu-item danger @click="handleUnsubscribe(record)">退订</a-menu-item>
|
||
</a-menu>
|
||
</template>
|
||
</a-dropdown>
|
||
</a-space>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-if="filteredPurchasedApps.length === 0 && !purchasedLoading" class="empty-state">
|
||
<a-empty description="暂无已购应用">
|
||
<template #image>
|
||
<div class="empty-icon">🛒</div>
|
||
</template>
|
||
<a-button type="primary" @click="navigateTo('/market')">去应用商店看看</a-button>
|
||
</a-empty>
|
||
</div>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<a-tab-pane key="store" tab="应用商店">
|
||
<div class="store-section">
|
||
<!-- 快捷入口卡片 -->
|
||
<div class="quick-cards">
|
||
<div class="quick-card" @click="navigateTo('/market')">
|
||
<div class="quick-card-icon blue">🛒</div>
|
||
<div class="quick-card-content">
|
||
<div class="quick-card-title">应用市场</div>
|
||
<div class="quick-card-desc">浏览和购买各类应用</div>
|
||
</div>
|
||
<RightOutlined class="quick-card-arrow" />
|
||
</div>
|
||
<div class="quick-card" @click="navigateTo('/market?type=plugin')">
|
||
<div class="quick-card-icon purple">🔌</div>
|
||
<div class="quick-card-content">
|
||
<div class="quick-card-title">插件中心</div>
|
||
<div class="quick-card-desc">扩展功能的插件</div>
|
||
</div>
|
||
<RightOutlined class="quick-card-arrow" />
|
||
</div>
|
||
<div class="quick-card" @click="navigateTo('/developer')">
|
||
<div class="quick-card-icon green">🛠️</div>
|
||
<div class="quick-card-content">
|
||
<div class="quick-card-title">开发者中心</div>
|
||
<div class="quick-card-desc">开发自己的应用</div>
|
||
</div>
|
||
<RightOutlined class="quick-card-arrow" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 推荐应用 -->
|
||
<div class="recommend-section">
|
||
<div class="section-header">
|
||
<h3>⭐ 推荐应用</h3>
|
||
<a-button type="link" @click="navigateTo('/market')">查看更多</a-button>
|
||
</div>
|
||
<div class="recommend-apps">
|
||
<div
|
||
v-for="app in recommendApps"
|
||
:key="app.productId"
|
||
class="recommend-card"
|
||
@click="navigateTo(`/market?app=${app.productId}`)"
|
||
>
|
||
<img v-if="app.icon" :src="app.icon" class="recommend-icon" />
|
||
<div v-else class="recommend-icon-placeholder">{{ (app.productName || 'A').charAt(0) }}</div>
|
||
<div class="recommend-info">
|
||
<div class="recommend-name">{{ app.productName }}</div>
|
||
<div class="recommend-desc">{{ app.description || '暂无描述' }}</div>
|
||
</div>
|
||
<div class="recommend-price">
|
||
<span v-if="app.priceType === 'free'" class="price-free">免费</span>
|
||
<span v-else class="price-paid">¥{{ (app.price || 0) / 100 }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
|
||
<!-- 应用配置弹窗 -->
|
||
<a-modal
|
||
v-model:open="configModalVisible"
|
||
title="应用配置"
|
||
width="600px"
|
||
@ok="handleSaveConfig"
|
||
@cancel="configModalVisible = false"
|
||
>
|
||
<a-form :model="configForm" layout="vertical">
|
||
<a-form-item label="应用名称">
|
||
<a-input v-model:value="configForm.appName" disabled />
|
||
</a-form-item>
|
||
<a-form-item label="启用状态">
|
||
<a-switch v-model:checked="configForm.enabled" />
|
||
</a-form-item>
|
||
<a-form-item label="自动续费">
|
||
<a-switch v-model:checked="configForm.autoRenew" />
|
||
</a-form-item>
|
||
<a-form-item label="配置参数">
|
||
<a-textarea v-model:value="configForm.config" :rows="5" placeholder="JSON 格式的配置参数" />
|
||
</a-form-item>
|
||
</a-form>
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ShopOutlined, RightOutlined, MoreOutlined } from '@ant-design/icons-vue'
|
||
import { message } from 'ant-design-vue'
|
||
import AppsCenter from '@/components/console/AppsCenter.vue'
|
||
import { pageAppProduct } from '@/api/app/appProduct'
|
||
import type { AppProduct } from '@/api/app/appProduct/model'
|
||
import { getUserInfo } from '@/api/layout'
|
||
import { pageShopOrder } from '@/api/shop/shopOrder'
|
||
import type { ShopOrder } from '@/api/shop/shopOrder/model'
|
||
|
||
definePageMeta({ layout: 'console' })
|
||
|
||
// 设置页面标题
|
||
useHead({
|
||
title: '应用中心'
|
||
})
|
||
|
||
// ─── 用户信息 ────────────────────────────────────────────────
|
||
const userId = ref<string | null>(import.meta.client ? localStorage.getItem('UserId') : null)
|
||
const userIdNum = computed(() => (userId.value ? Number(userId.value) : 0))
|
||
|
||
async function ensureUser() {
|
||
if (userIdNum.value) return
|
||
try {
|
||
const user = await getUserInfo()
|
||
const uid = user.userId
|
||
if (uid) {
|
||
userId.value = String(uid)
|
||
}
|
||
} catch {}
|
||
}
|
||
|
||
// Tab 状态
|
||
const activeTab = ref('my-apps')
|
||
|
||
// 已购应用状态
|
||
const purchasedLoading = ref(false)
|
||
const purchasedSearch = ref('')
|
||
const purchasedFilter = ref('')
|
||
|
||
// 推荐应用
|
||
const recommendApps = ref<AppProduct[]>([])
|
||
|
||
// ─── 已购应用数据(从订单API加载) ──────────────────────────
|
||
interface PurchasedApp {
|
||
id: number
|
||
orderId: number
|
||
orderNo: string
|
||
appName: string
|
||
appIcon?: string
|
||
developerName: string
|
||
priceType: 'free' | 'one_time' | 'subscription'
|
||
price: number
|
||
status: 'active' | 'expired' | 'cancelled'
|
||
enabled: boolean
|
||
startTime: string
|
||
endTime?: string
|
||
autoRenew: boolean
|
||
config?: string
|
||
payPrice?: string
|
||
}
|
||
|
||
const purchasedApps = ref<PurchasedApp[]>([])
|
||
|
||
// 产品名称映射
|
||
const productCatalog: Record<string, string> = {
|
||
website: '企业官网',
|
||
shop: '电商系统',
|
||
mp: '小程序/公众号',
|
||
}
|
||
|
||
function resolveProductCode(order?: ShopOrder | null) {
|
||
if (!order?.description) return ''
|
||
try {
|
||
const extra = JSON.parse(order.description)
|
||
return typeof extra?.product === 'string' ? extra.product.trim() : ''
|
||
} catch {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
function resolveProductName(order?: ShopOrder | null) {
|
||
const code = resolveProductCode(order)
|
||
if (code && productCatalog[code]) return productCatalog[code]
|
||
if (code) return code
|
||
return '应用服务'
|
||
}
|
||
|
||
function resolveMonths(order?: ShopOrder | null) {
|
||
if (!order?.description) return 0
|
||
try {
|
||
const extra = JSON.parse(order.description)
|
||
return typeof extra?.months === 'number' ? extra.months : 0
|
||
} catch {
|
||
return 0
|
||
}
|
||
}
|
||
|
||
function resolveTenantName(order?: ShopOrder | null) {
|
||
if (!order?.description) return ''
|
||
try {
|
||
const extra = JSON.parse(order.description)
|
||
return typeof extra?.tenantName === 'string' ? extra.tenantName.trim() : ''
|
||
} catch {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
function isExpired(expirationTime?: string) {
|
||
if (!expirationTime) return false
|
||
return new Date(expirationTime).getTime() < Date.now()
|
||
}
|
||
|
||
function determinePriceType(order: ShopOrder): 'free' | 'one_time' | 'subscription' {
|
||
const months = resolveMonths(order)
|
||
if (Number(order.payType) === 12 || Number(order.payPrice) === 0) return 'free'
|
||
if (months > 0) return 'subscription'
|
||
return 'one_time'
|
||
}
|
||
|
||
function transformOrderToApp(order: ShopOrder): PurchasedApp {
|
||
const expired = isExpired(order.expirationTime)
|
||
const cancelled = Number(order.orderStatus) === 2 || Number(order.orderStatus) === 3
|
||
return {
|
||
id: order.orderId || 0,
|
||
orderId: order.orderId || 0,
|
||
orderNo: order.orderNo || '',
|
||
appName: resolveProductName(order),
|
||
developerName: resolveTenantName(order) || '官方',
|
||
priceType: determinePriceType(order),
|
||
price: Number(order.payPrice || order.totalPrice || 0),
|
||
status: cancelled ? 'cancelled' : expired ? 'expired' : 'active',
|
||
enabled: !expired && !cancelled && Number(order.orderStatus) === 1,
|
||
startTime: order.createTime ? String(order.createTime).replace(/T.*/, '') : '',
|
||
endTime: order.expirationTime ? String(order.expirationTime).replace(/T.*/, '') : undefined,
|
||
autoRenew: resolveMonths(order) > 1,
|
||
payPrice: order.payPrice || order.totalPrice,
|
||
}
|
||
}
|
||
|
||
// 表格列
|
||
const purchasedColumns = [
|
||
{ title: '应用信息', key: 'appInfo', width: 280 },
|
||
{ title: '订阅信息', key: 'subscription', width: 200 },
|
||
{ title: '状态', key: 'status', width: 120 },
|
||
{ title: '操作', key: 'action', width: 200 },
|
||
]
|
||
|
||
// 配置弹窗
|
||
const configModalVisible = ref(false)
|
||
const configForm = reactive({
|
||
appName: '',
|
||
enabled: true,
|
||
autoRenew: false,
|
||
config: '',
|
||
})
|
||
|
||
// 计算属性
|
||
const filteredPurchasedApps = computed(() => {
|
||
let result = [...purchasedApps.value]
|
||
if (purchasedFilter.value) {
|
||
result = result.filter(app => app.status === purchasedFilter.value)
|
||
}
|
||
if (purchasedSearch.value) {
|
||
const kw = purchasedSearch.value.toLowerCase()
|
||
result = result.filter(app =>
|
||
app.appName.toLowerCase().includes(kw) ||
|
||
app.developerName.toLowerCase().includes(kw)
|
||
)
|
||
}
|
||
return result
|
||
})
|
||
|
||
// 加载推荐应用
|
||
async function loadRecommendApps() {
|
||
try {
|
||
const res = await pageAppProduct({
|
||
page: 1,
|
||
limit: 4,
|
||
status: 1,
|
||
})
|
||
recommendApps.value = res.list || []
|
||
} catch (e) {
|
||
console.error('加载推荐应用失败', e)
|
||
}
|
||
}
|
||
|
||
// 加载已购应用(从订单API)
|
||
async function loadPurchasedApps() {
|
||
purchasedLoading.value = true
|
||
try {
|
||
await ensureUser()
|
||
const uid = userIdNum.value
|
||
if (!uid) {
|
||
purchasedApps.value = []
|
||
return
|
||
}
|
||
|
||
// 加载已支付且已完成的订单,作为已购应用列表
|
||
const data = await pageShopOrder({
|
||
page: 1,
|
||
limit: 100,
|
||
userId: uid,
|
||
payStatus: 1, // 已支付
|
||
})
|
||
|
||
const list = data?.list || []
|
||
purchasedApps.value = list.map(transformOrderToApp)
|
||
} catch (e) {
|
||
console.error('加载已购应用失败', e)
|
||
purchasedApps.value = []
|
||
} finally {
|
||
purchasedLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 处理函数
|
||
function handleTabChange(key: string) {
|
||
if (key === 'store') {
|
||
loadRecommendApps()
|
||
} else if (key === 'purchased') {
|
||
loadPurchasedApps()
|
||
}
|
||
}
|
||
|
||
function priceTypeText(type: string) {
|
||
const map: Record<string, string> = {
|
||
free: '免费',
|
||
one_time: '一次性',
|
||
subscription: '订阅',
|
||
}
|
||
return map[type] || type
|
||
}
|
||
|
||
function subscriptionStatusColor(status: string) {
|
||
const map: Record<string, string> = {
|
||
active: 'success',
|
||
expired: 'error',
|
||
cancelled: 'default',
|
||
}
|
||
return map[status] || 'default'
|
||
}
|
||
|
||
function subscriptionStatusText(status: string) {
|
||
const map: Record<string, string> = {
|
||
active: '生效中',
|
||
expired: '已过期',
|
||
cancelled: '已取消',
|
||
}
|
||
return map[status] || status
|
||
}
|
||
|
||
function iconBgColor(name?: string) {
|
||
const PALETTE = ['#4e6ef2', '#f4a261', '#e76f51', '#2a9d8f', '#e9c46a']
|
||
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 handleToggleApp(record: PurchasedApp, enabled: boolean) {
|
||
message.success(`${record.appName} 已${enabled ? '启用' : '禁用'}`)
|
||
}
|
||
|
||
function handleEnterApp(record: PurchasedApp) {
|
||
navigateTo('/market')
|
||
}
|
||
|
||
function handleConfig(record: PurchasedApp) {
|
||
configForm.appName = record.appName
|
||
configForm.enabled = record.enabled
|
||
configForm.autoRenew = record.autoRenew
|
||
configForm.config = record.config || '{}'
|
||
configModalVisible.value = true
|
||
}
|
||
|
||
function handleSaveConfig() {
|
||
message.success('配置保存成功')
|
||
configModalVisible.value = false
|
||
}
|
||
|
||
function handleRenew(record: PurchasedApp) {
|
||
navigateTo('/market')
|
||
}
|
||
|
||
function handleViewDetail(record: PurchasedApp) {
|
||
navigateTo('/console/orders')
|
||
}
|
||
|
||
function handleUnsubscribe(record: PurchasedApp) {
|
||
navigateTo('/tickets')
|
||
}
|
||
|
||
onMounted(() => {
|
||
ensureUser()
|
||
loadRecommendApps()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.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;
|
||
}
|
||
|
||
/* Tab 样式 */
|
||
.apps-tabs :deep(.ant-tabs-nav) {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
/* 已购应用 */
|
||
.purchased-section {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.filter-bar {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.app-info-text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.app-name {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: rgba(0, 0, 0, 0.85);
|
||
}
|
||
|
||
.app-developer {
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
.subscription-cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.expire-time {
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
/* 应用商店 */
|
||
.store-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
}
|
||
|
||
.quick-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.quick-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
padding: 20px;
|
||
background: #fff;
|
||
border: 1px solid #f0f0f0;
|
||
border-radius: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.quick-card:hover {
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||
border-color: #d9d9d9;
|
||
}
|
||
|
||
.quick-card-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.quick-card-icon.blue { background: #eff6ff; }
|
||
.quick-card-icon.purple { background: #f5f3ff; }
|
||
.quick-card-icon.green { background: #f0fdf4; }
|
||
|
||
.quick-card-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.quick-card-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: rgba(0, 0, 0, 0.85);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.quick-card-desc {
|
||
font-size: 13px;
|
||
color: rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
.quick-card-arrow {
|
||
color: rgba(0, 0, 0, 0.25);
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.quick-card:hover .quick-card-arrow {
|
||
transform: translateX(4px);
|
||
color: rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
/* 推荐应用 */
|
||
.recommend-section {
|
||
background: #fff;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
border: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.section-header h3 {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.recommend-apps {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.recommend-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
background: #fafafa;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.recommend-card:hover {
|
||
background: #f0f0f0;
|
||
}
|
||
|
||
.recommend-icon {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 10px;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.recommend-icon-placeholder {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 10px;
|
||
background: #4e6ef2;
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.recommend-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.recommend-name {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: rgba(0, 0, 0, 0.85);
|
||
margin-bottom: 4px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.recommend-desc {
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.45);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.recommend-price {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.price-free {
|
||
color: #22c55e;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.price-paid {
|
||
color: #f59e0b;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 空状态 */
|
||
.empty-state {
|
||
padding: 60px 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.ml-2 { margin-left: 8px; }
|
||
</style>
|