feat: P3阶段完成 - 全部模块迁移完成 🎉
## P3.1: 争议仲裁模块(disputes)✅ - API: disputes.ts - 模块导出 ## P3.2: 卖家中心模块(seller)✅ - Views: 4个页面 - Composables: usePublishForm, usePublishDraft - 模块导出 ## P3.3: 管理后台模块(admin)✅ - API: 8个文件(adminAuth, adminDashboard, adminUsers等) - Views: 15个管理页面 - Composables: useAdminTable, useAdminPaginatedTable - Components: 管理端组件 - 模块导出 --- ## 🎉 Features 架构迁移全部完成! ### 最终统计 - ✅ P0: shared(基础设施)- 22个文件 - ✅ P1: wallet, chats, orders - 24个文件 - ✅ P2: listings, auth - 35个文件 - ✅ P3: seller, disputes, admin - 47个文件 **总计:** 9个模块,128个文件完成迁移 ### 新架构 ``` frontend/src/ ├── features/ # 9个业务模块 ✅ │ ├── wallet/ ✅ │ ├── chats/ ✅ │ ├── orders/ ✅ (已重构) │ ├── listings/ ✅ │ ├── auth/ ✅ │ ├── seller/ ✅ │ ├── disputes/ ✅ │ └── admin/ ✅ └── shared/ ✅ ``` 下一步:清理旧文件、更新路由配置 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
3534cffce1
commit
c9397635e2
@@ -0,0 +1,406 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
emptyListingSalePriceConfig,
|
||||
emptyListingPublishOptions,
|
||||
mergeListingSalePriceConfig,
|
||||
mergeListingPublishOptions,
|
||||
type ListingPublishOptions,
|
||||
type PublishSalePriceConfig,
|
||||
} from '@/api/listingOptions'
|
||||
import {
|
||||
defaultHomeAnnouncements,
|
||||
defaultHomeBanners,
|
||||
mergeHomeConfig,
|
||||
type HomeBannerSlide,
|
||||
} from '@/api/homeConfig'
|
||||
import { fetchSystemConfigs, type SystemConfig } from '@/api/systemConfigs'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
import { safeParseJSON } from '@/utils/json'
|
||||
import { formatSystemConfigSelectValue } from '@/utils/systemConfigOptions'
|
||||
|
||||
// 导入重构后的 Dialog 子组件
|
||||
import PublishOptionsDialog from './components/PublishOptionsDialog.vue'
|
||||
import SalePriceDialog from './components/SalePriceDialog.vue'
|
||||
import HomeAnnouncementsDialog from './components/HomeAnnouncementsDialog.vue'
|
||||
import HomeBannersDialog from './components/HomeBannersDialog.vue'
|
||||
import GeneralConfigDialog from './components/GeneralConfigDialog.vue'
|
||||
import AutoWelcomeConfig from './components/AutoWelcomeConfig.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const configs = ref<SystemConfig[]>([])
|
||||
const currentEditingConfig = ref<SystemConfig | null>(null)
|
||||
|
||||
// 各种子 Dialog 的可见性控制
|
||||
const publishVisible = ref(false)
|
||||
const salePriceVisible = ref(false)
|
||||
const announcementsVisible = ref(false)
|
||||
const bannersVisible = ref(false)
|
||||
const generalVisible = ref(false)
|
||||
|
||||
const publishConfig = computed(() => configs.value.find((item) => item.key === 'listing.publish_options') || null)
|
||||
const salePriceConfig = computed(() => configs.value.find((item) => item.key === 'listing.sale_price_config') || null)
|
||||
const homeAnnouncementsConfig = computed(() => configs.value.find((item) => item.key === 'mobile.home_announcements') || null)
|
||||
const homeBannersConfig = computed(() => configs.value.find((item) => item.key === 'mobile.home_banners') || null)
|
||||
|
||||
const regularConfigs = computed(() =>
|
||||
configs.value.filter(
|
||||
(item) =>
|
||||
item.key !== 'listing.publish_options' &&
|
||||
item.key !== 'listing.sale_price_config' &&
|
||||
item.key !== 'mobile.home_announcements' &&
|
||||
item.key !== 'mobile.home_banners',
|
||||
),
|
||||
)
|
||||
|
||||
// 只读计算属性,用于渲染卡片上的统计信息
|
||||
const publishStats = computed(() => {
|
||||
const options = safeParsePublishOptions(publishConfig.value?.value || '')
|
||||
return {
|
||||
baseCount:
|
||||
options.server_options.length +
|
||||
options.rank_options.length +
|
||||
options.insurance_options.length +
|
||||
options.level_options.length,
|
||||
skinCount: options.skin_groups.reduce((sum, group) => sum + group.options.length, 0),
|
||||
resourceCount: options.quantity_items.length,
|
||||
screenshotCount: options.screenshot_slots.length,
|
||||
regionCount: options.region_options.length,
|
||||
}
|
||||
})
|
||||
|
||||
const salePriceStats = computed(() => {
|
||||
const config = safeParseSalePriceConfig(salePriceConfig.value?.value || '')
|
||||
return {
|
||||
fixedCount: config.fixed_markup_rules.length,
|
||||
ratioCount: config.ratio_adjustment_rules.length,
|
||||
}
|
||||
})
|
||||
|
||||
const homeStats = computed(() => {
|
||||
const announcements = parseHomeAnnouncements(homeAnnouncementsConfig.value?.value || '')
|
||||
const banners = parseHomeBanners(homeBannersConfig.value?.value || '')
|
||||
return {
|
||||
announcementCount: announcements.length,
|
||||
bannerCount: banners.length,
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(loadConfigs)
|
||||
|
||||
async function loadConfigs() {
|
||||
loading.value = true
|
||||
try {
|
||||
configs.value = await fetchSystemConfigs()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openEdit(row: SystemConfig) {
|
||||
currentEditingConfig.value = row
|
||||
if (row.key === 'listing.publish_options') {
|
||||
publishVisible.value = true
|
||||
} else if (row.key === 'listing.sale_price_config') {
|
||||
salePriceVisible.value = true
|
||||
} else if (row.key === 'mobile.home_announcements') {
|
||||
announcementsVisible.value = true
|
||||
} else if (row.key === 'mobile.home_banners') {
|
||||
bannersVisible.value = true
|
||||
} else {
|
||||
generalVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function safeParsePublishOptions(raw: string) {
|
||||
const parsed = safeParseJSON(raw, emptyListingPublishOptions)
|
||||
return cloneOptions(mergeListingPublishOptions(parsed))
|
||||
}
|
||||
|
||||
function safeParseSalePriceConfig(raw: string) {
|
||||
const parsed = safeParseJSON(raw, emptyListingSalePriceConfig)
|
||||
return cloneSalePriceConfig(mergeListingSalePriceConfig(parsed))
|
||||
}
|
||||
|
||||
function parseHomeAnnouncements(raw: string) {
|
||||
const parsed = safeParseJSON(raw, defaultHomeAnnouncements)
|
||||
return mergeHomeConfig({ announcements: Array.isArray(parsed) ? parsed : [] }).announcements
|
||||
}
|
||||
|
||||
function parseHomeBanners(raw: string) {
|
||||
const parsed = safeParseJSON(raw, defaultHomeBanners)
|
||||
return cloneHomeBanners(mergeHomeConfig({ banners: Array.isArray(parsed) ? parsed : [] }).banners)
|
||||
}
|
||||
|
||||
function cloneOptions(options: ListingPublishOptions) {
|
||||
return JSON.parse(JSON.stringify(options)) as ListingPublishOptions
|
||||
}
|
||||
|
||||
function cloneSalePriceConfig(config: PublishSalePriceConfig) {
|
||||
return JSON.parse(JSON.stringify(config)) as PublishSalePriceConfig
|
||||
}
|
||||
|
||||
function cloneHomeBanners(banners: HomeBannerSlide[]) {
|
||||
return JSON.parse(JSON.stringify(banners)) as HomeBannerSlide[]
|
||||
}
|
||||
|
||||
function formatHomeConfigStatus(row: SystemConfig | null, fallback: string) {
|
||||
if (!row) return fallback
|
||||
return formatDateTime(row.updated_at, fallback)
|
||||
}
|
||||
|
||||
function formatConfigValue(row: SystemConfig) {
|
||||
const trimmed = row.value.trim()
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
return 'JSON 配置'
|
||||
}
|
||||
const selectedLabel = formatSystemConfigSelectValue(row.key, row.value)
|
||||
if (selectedLabel) {
|
||||
return selectedLabel
|
||||
}
|
||||
return row.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Configs</p>
|
||||
<h1>系统配置</h1>
|
||||
<p>管理交接超时、归还超时、短信限流、最低押金和抽成比例。</p>
|
||||
</div>
|
||||
|
||||
<section v-if="publishConfig" class="publish-config-panel">
|
||||
<div class="publish-config-main">
|
||||
<div>
|
||||
<p class="eyebrow">Publish Options</p>
|
||||
<h2>发布表单选项</h2>
|
||||
<span>管理移动端发布页的区服、段位、皮肤、额外消耗品、截图材料和地区选项。</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="openEdit(publishConfig)">编辑发布选项</el-button>
|
||||
</div>
|
||||
<div class="publish-stat-grid">
|
||||
<div class="publish-stat">
|
||||
<strong>{{ publishStats.baseCount }}</strong>
|
||||
<span>基础选项</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>{{ publishStats.skinCount }}</strong>
|
||||
<span>皮肤项</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>{{ publishStats.resourceCount }}</strong>
|
||||
<span>额外消耗品</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>{{ publishStats.screenshotCount }}</strong>
|
||||
<span>截图材料</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>{{ publishStats.regionCount }}</strong>
|
||||
<span>地区</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="salePriceConfig" class="publish-config-panel">
|
||||
<div class="publish-config-main">
|
||||
<div>
|
||||
<p class="eyebrow">Sale Pricing</p>
|
||||
<h2>内部出售定价规则</h2>
|
||||
<span>管理出售专用比例计算规则,仅用于平台内部定价计算,不在发布页展示。</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="openEdit(salePriceConfig)">编辑出售规则</el-button>
|
||||
</div>
|
||||
<div class="publish-stat-grid home-stat-grid">
|
||||
<div class="publish-stat">
|
||||
<strong>{{ salePriceStats.fixedCount }}</strong>
|
||||
<span>固定加价</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>{{ salePriceStats.ratioCount }}</strong>
|
||||
<span>比例修正</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>内部</strong>
|
||||
<span>不展示给发布用户</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>配置</strong>
|
||||
<span>listing.sale_price_config</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>更新</strong>
|
||||
<span>{{ formatHomeConfigStatus(salePriceConfig, '未初始化') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="homeAnnouncementsConfig || homeBannersConfig" class="publish-config-panel">
|
||||
<div class="publish-config-main">
|
||||
<div>
|
||||
<p class="eyebrow">Home Content</p>
|
||||
<h2>移动端首页运营配置</h2>
|
||||
<span>管理首页公告滚动内容和顶部轮播图,保存后移动端会从接口读取最新配置。</span>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<el-button v-if="homeAnnouncementsConfig" @click="openEdit(homeAnnouncementsConfig)">编辑公告</el-button>
|
||||
<el-button v-if="homeBannersConfig" type="primary" @click="openEdit(homeBannersConfig)">编辑轮播图</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="publish-stat-grid home-stat-grid">
|
||||
<div class="publish-stat">
|
||||
<strong>{{ homeStats.announcementCount }}</strong>
|
||||
<span>公告条数</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>{{ homeStats.bannerCount }}</strong>
|
||||
<span>轮播图</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>接口</strong>
|
||||
<span>/api/mobile-home-config</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>公告</strong>
|
||||
<span>{{ formatHomeConfigStatus(homeAnnouncementsConfig, '未初始化') }}</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>轮播</strong>
|
||||
<span>{{ formatHomeConfigStatus(homeBannersConfig, '未初始化') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<AutoWelcomeConfig />
|
||||
|
||||
<el-table v-loading="loading" class="table-panel" :data="regularConfigs">
|
||||
<el-table-column prop="key" label="配置项" min-width="260" />
|
||||
<el-table-column label="当前值" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="formatConfigValue(row) === 'JSON 配置'" type="info">JSON 配置</el-tag>
|
||||
<span v-else class="config-value">{{ formatConfigValue(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="说明" min-width="260" show-overflow-tooltip />
|
||||
<el-table-column prop="updated_by" label="更新人" width="100" />
|
||||
<el-table-column label="更新时间" min-width="180">
|
||||
<template #default="{ row }">{{ formatDateTime(row.updated_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEdit(row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 弹窗编辑器子组件 -->
|
||||
<PublishOptionsDialog
|
||||
v-if="publishConfig"
|
||||
v-model="publishVisible"
|
||||
:config="publishConfig"
|
||||
@saved="loadConfigs"
|
||||
/>
|
||||
|
||||
<SalePriceDialog
|
||||
v-if="salePriceConfig"
|
||||
v-model="salePriceVisible"
|
||||
:config="salePriceConfig"
|
||||
@saved="loadConfigs"
|
||||
/>
|
||||
|
||||
<HomeAnnouncementsDialog
|
||||
v-if="homeAnnouncementsConfig"
|
||||
v-model="announcementsVisible"
|
||||
:config="homeAnnouncementsConfig"
|
||||
@saved="loadConfigs"
|
||||
/>
|
||||
|
||||
<HomeBannersDialog
|
||||
v-if="homeBannersConfig"
|
||||
v-model="bannersVisible"
|
||||
:config="homeBannersConfig"
|
||||
@saved="loadConfigs"
|
||||
/>
|
||||
|
||||
<GeneralConfigDialog
|
||||
v-if="currentEditingConfig"
|
||||
v-model="generalVisible"
|
||||
:config="currentEditingConfig"
|
||||
@saved="loadConfigs"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.publish-config-panel {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
margin-bottom: 18px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.publish-config-main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.publish-config-main h2 {
|
||||
margin: 4px 0 6px;
|
||||
color: #111827;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.publish-config-main span {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.publish-stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.home-stat-grid .publish-stat strong {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.publish-stat {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.publish-stat strong {
|
||||
color: #1477ff;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.publish-stat span,
|
||||
.config-value {
|
||||
color: #4b5563;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.publish-stat-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user