feat(home): 重构首页界面并移除文章相关页面

- 添加公司信息配置文件,包含项目名称、地址、经营范围等
- 实现404页面路由,显示页面建设中提示和导航按钮
- 在首页集成公司信息展示,包括经营范围和资质信息
- 移除文章列表页、文章详情页、栏目页和单页内容相关功能
- 更新Ant Design主题配色为绿色主色调
- 简化首页布局,突出业务板块和服务导向设计
- 删除部署方案和开通流程等临时页面内容
This commit is contained in:
2026-01-27 09:51:14 +08:00
parent 775841eed3
commit a83f2969d8
17 changed files with 423 additions and 1446 deletions

View File

@@ -1,7 +1,16 @@
<template>
<NuxtLayout>
<NuxtRouteAnnouncer />
<NuxtPage />
</NuxtLayout>
<a-config-provider :theme="theme">
<NuxtLayout>
<NuxtRouteAnnouncer />
<NuxtPage />
</NuxtLayout>
</a-config-provider>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
const theme = {
token: {
// Ant Design Vue primary color -> green
colorPrimary: '#16a34a'
}
}
</script>

View File

@@ -17,10 +17,8 @@
<a-col :xs="24" :md="8">
<div class="text-base font-semibold text-white">快速入口</div>
<div class="mt-4 grid gap-2 text-sm text-gray-400">
<NuxtLink class="hover:text-white" to="/platform">平台能力</NuxtLink>
<NuxtLink class="hover:text-white" to="/products">产品矩阵</NuxtLink>
<NuxtLink class="hover:text-white" to="/market">模板/插件市场</NuxtLink>
<NuxtLink class="hover:text-white" to="/deploy">部署方案</NuxtLink>
<NuxtLink class="hover:text-white" to="/products">经营范围</NuxtLink>
<NuxtLink class="hover:text-white" to="/contact">联系我们</NuxtLink>
</div>
</a-col>
@@ -30,8 +28,21 @@
</a-col>
</a-row>
<div class="mt-10 border-t border-white/10 pt-6 text-xs text-gray-500">
© {{ year }} {{ siteName }}. All rights reserved.
<div
class="mt-10 flex flex-col gap-2 border-t border-white/10 pt-6 text-xs text-gray-500 md:flex-row md:items-center md:justify-between"
>
<div>© {{ year }} {{ siteName }}. All rights reserved.</div>
<div class="tools flex items-center opacity-80 hover:opacity-90 text-gray-100 text-xs">
Powered by
<a
rel="nofollow"
href="https://site.websoft.top"
target="_blank"
class="text-white visited:text-white hover:text-gray-200 ml-1"
>
·企业官网
</a>
</div>
</div>
</div>
</a-layout-footer>
@@ -53,4 +64,3 @@ const year = new Date().getFullYear()
padding: 0;
}
</style>

View File

@@ -7,38 +7,9 @@
</div>
<div class="topbar-right">
<a-input-search
v-model:value="keywords"
class="topbar-search"
placeholder="请输入关键字"
:allow-clear="true"
@search="onSearch"
/>
<div class="hidden md:flex items-center gap-3">
<template v-if="!isAuthed">
<a-button size="small" type="primary" @click="navigateTo('/login')">登录</a-button>
</template>
<template v-else>
<a-dropdown :trigger="['hover']" placement="bottomRight">
<a-space>
<a-avatar :src="userAvatar" :size="28">
<template v-if="!userAvatar" #icon>
<UserOutlined />
</template>
</a-avatar>
<span class="topbar-user">{{ userName }}</span>
</a-space>
<template #overlay>
<a-menu @click="onUserMenuClick">
<a-menu-item key="console">管理中心</a-menu-item>
<a-menu-item key="profile">个人资料</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout">退出登录</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<div class="hidden md:flex items-center gap-2">
<a-button size="small" @click="navigateTo('/products')">经营范围</a-button>
<a-button size="small" type="primary" @click="navigateTo('/contact')">联系我们</a-button>
</div>
<a-button class="md:hidden" size="small" @click="open = true">菜单</a-button>
@@ -137,30 +108,21 @@
</a-menu>
<div class="mt-4">
<a-button v-if="!isAuthed" block type="primary" @click="onNav('/login')">登录</a-button>
<template v-else>
<a-button block @click="goConsoleCenter">管理中心</a-button>
<a-button block @click="goDeveloperCenter">开发者中心</a-button>
<a-button block @click="onNav('/profile')">个人资料</a-button>
<a-button block danger class="mt-2" @click="logout">退出登录</a-button>
</template>
<a-space direction="vertical" class="w-full">
<a-button type="primary" block @click="onNav('/contact')">联系我们</a-button>
<a-button block @click="onNav('/products')">经营范围</a-button>
</a-space>
</div>
</a-drawer>
</template>
<script setup lang="ts">
import { mainNav } from '@/config/nav'
import { getUserInfo } from '@/api/layout'
import type { User } from '@/api/system/user/model'
import type { CmsNavigation } from '@/api/cms/cmsNavigation/model'
import { getToken, removeToken } from '@/utils/token-util'
import { clearAuthz, hasRole, setAuthzFromUser } from '@/utils/permission'
import { UserOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { COMPANY } from '@/config/company'
const route = useRoute()
const open = ref(false)
const keywords = ref('')
type HeaderNavItem = {
key: string
@@ -218,10 +180,8 @@ const siteName = computed(() => {
})
const showBrandbar = computed(() => {
const p = route.path || '/'
if (p === '/') return true
// 文章列表、单页详情、文章详情都显示 brandbar
return p === '/articles' || p.startsWith('/article/') || p.startsWith('/page/') || p.startsWith('/item/')
// Corporate site: keep brand bar consistent across pages.
return true
})
const logoUrl = computed(() => {
@@ -241,10 +201,10 @@ const siteSlogan = computed(() => {
pickString(data?.setting, 'slogan') ||
pickString(data?.setting, 'subtitle') ||
pickString(data?.config, 'slogan')
return slogan || 'XINGYUSI BANKRUPTCY TRANSACTION SERVICE PLATFORM'
return slogan || `${COMPANY.projectName} · 品质服务与合规经营`
})
const missionText = computed(() => '致力于企业纾困和破产事务服务')
const missionText = computed(() => '生物基材料研发 · 技术服务 · 食品与农产品流通')
const valuesText = computed(() => '真诚 · 奉献 · 规范 · 聚力')
function normalizePath(path: unknown) {
@@ -290,6 +250,7 @@ function normalizeNavTree(list: CmsNavigation[]): HeaderNavItem[] {
const navItems = computed<HeaderNavItem[]>(() => {
const apiNavs = siteData.value?.topNavs
if (Array.isArray(apiNavs) && apiNavs.length) {
// Prefer navigation from getShopInfo (CMS-managed).
return normalizeNavTree(apiNavs as CmsNavigation[])
}
// Fallback when CMS has not configured topNavs.
@@ -313,28 +274,6 @@ function onNavClick(item: HeaderNavItem) {
if (item.to) navigateTo(item.to)
}
function onSearch() {
if (!keywords.value.trim()) return
navigateTo({ path: '/articles', query: { keywords: keywords.value.trim() } })
}
const token = ref('')
const user = ref<User | null>(null)
const isAuthed = computed(() => !!token.value)
const userName = computed(() => String(user.value?.nickname || user.value?.username || '已登录'))
const userAvatar = computed(() => {
const candidate =
user.value?.avatarUrl ||
user.value?.avatar ||
user.value?.merchantAvatar ||
user.value?.logo ||
''
if (typeof candidate !== 'string') return ''
const normalized = candidate.trim()
if (!normalized || normalized === 'null' || normalized === 'undefined') return ''
return normalized
})
function onNav(to: string) {
open.value = false
navigateTo(to)
@@ -346,69 +285,6 @@ const todayText = computed(() => {
const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}日 星期${week}`
})
async function refreshAuth() {
token.value = getToken()
if (!token.value) {
user.value = null
clearAuthz()
return
}
try {
user.value = await getUserInfo()
setAuthzFromUser(user.value)
} catch {
// token may be expired; keep authed UI but without profile info
clearAuthz()
}
}
function goConsoleCenter() {
if (!isAuthed.value) return navigateTo('/login')
open.value = false
navigateTo('/console')
}
function goDeveloperCenter() {
if (!isAuthed.value) return navigateTo('/login')
open.value = false
if (!hasRole('developer')) return message.error('您还不是开发者')
navigateTo('/developer')
}
function logout() {
removeToken()
try {
localStorage.removeItem('TenantId')
localStorage.removeItem('UserId')
} catch {
// ignore
}
clearAuthz()
user.value = null
token.value = ''
open.value = false
navigateTo('/')
}
function onUserMenuClick(info: { key: string }) {
if (info.key === 'console') return goConsoleCenter()
if (info.key === 'developer') return goDeveloperCenter()
if (info.key === 'profile') return navigateTo('/profile')
if (info.key === 'logout') return logout()
}
onMounted(() => {
refreshAuth()
window.addEventListener('auth-token-changed', refreshAuth)
window.addEventListener('storage', refreshAuth)
})
onUnmounted(() => {
window.removeEventListener('auth-token-changed', refreshAuth)
window.removeEventListener('storage', refreshAuth)
})
</script>
<style scoped>
@@ -434,14 +310,6 @@ onUnmounted(() => {
padding: 6px 0;
}
.topbar-search {
width: 240px;
}
.topbar-user {
color: rgba(0, 0, 0, 0.85);
}
.brandbar {
background:
linear-gradient(0deg, rgba(255, 255, 255, 0.88), rgba(255, 255, 255, 0.88)),
@@ -468,7 +336,7 @@ onUnmounted(() => {
font-size: 28px;
line-height: 1.1;
font-weight: 800;
color: #b91c1c;
color: #15803d;
}
.brand-sub {
@@ -491,7 +359,7 @@ onUnmounted(() => {
}
.navbar {
background: #c30000;
background: #16a34a;
border-top: 1px solid rgba(255, 255, 255, 0.15);
border-bottom: 1px solid rgba(0, 0, 0, 0.15);
}

11
app/config/company.ts Normal file
View File

@@ -0,0 +1,11 @@
export const COMPANY = {
projectName: '桂乐淘',
address: '南宁市江南区国凯大道东13号神冠胶原智库项目加工厂房',
scope: {
general:
'生物基材料技术研发;技术服务、技术开发、技术咨询、技术交流、技术转让、技术推广;食品销售(仅销售预包装食品);保健食品(预包装)销售;鲜肉零售;新鲜水果批发;特殊医学用途配方食品销售;水产品零售;鲜肉批发;鲜蛋零售;食品互联网销售(仅销售预包装食品);食用农产品零售;新鲜水果零售;新鲜蔬菜零售;鲜蛋批发(除依法须经批准的项目外,凭营业执照依法自主开展经营活动)。',
licensed:
'食品销售;酒类经营;食品互联网销售;食品经营管理(依法须经批准的项目,经相关部门批准后方可开展经营活动,具体经营项目以相关部门批准文件或许可证件为准)。'
},
tags: ['生物基材料', '技术服务', '食品/农产品流通', '合规经营']
} as const

View File

@@ -6,10 +6,6 @@ export type NavItem = {
export const mainNav: NavItem[] = [
{key: 'home', label: '首页', to: '/'},
{key: 'products', label: '产品矩阵', to: '/products'},
{key: 'platform', label: '平台能力', to: '/platform'},
{key: 'market', label: '模板/插件市场', to: '/market'},
{key: 'deploy', label: '部署方案', to: '/deploy'},
{key: 'flow', label: '开通流程', to: '/flow'},
{key: 'products', label: '经营范围', to: '/products'},
{key: 'contact', label: '联系我们', to: '/contact'}
]

33
app/pages/[...slug].vue Normal file
View File

@@ -0,0 +1,33 @@
<template>
<div class="mx-auto max-w-screen-md px-4 py-16">
<a-result status="404" title="页面建设中" sub-title="该页面暂未开放建议返回首页或联系我们">
<template #extra>
<a-space>
<a-button type="primary" @click="navigateTo('/')">返回首页</a-button>
<a-button @click="navigateTo('/products')">经营范围</a-button>
<a-button @click="navigateTo('/contact')">联系我们</a-button>
</a-space>
</template>
</a-result>
<a-card class="mt-6" size="small">
<div class="text-sm text-gray-500">
当前路径<span class="font-mono">{{ path }}</span>
</div>
</a-card>
</div>
</template>
<script setup lang="ts">
import { usePageSeo } from '@/composables/usePageSeo'
const route = useRoute()
const path = computed(() => route.fullPath || '/')
usePageSeo({
title: '页面未开放',
description: '该页面暂未开放,可返回首页或进入联系我们/经营范围。',
path: route.path
})
</script>

View File

@@ -1,193 +0,0 @@
<template>
<main class="min-h-screen bg-gray-50 px-4 py-8">
<div class="mx-auto max-w-screen-lg space-y-6">
<a-breadcrumb>
<a-breadcrumb-item>
<NuxtLink to="/">首页</NuxtLink>
</a-breadcrumb-item>
<a-breadcrumb-item>栏目</a-breadcrumb-item>
<a-breadcrumb-item>{{ navTitle }}</a-breadcrumb-item>
</a-breadcrumb>
<a-card class="shadow-sm">
<template #title>
<div class="space-y-1">
<div class="text-xl font-semibold text-gray-900">{{ navTitle }}</div>
<div v-if="navDescription" class="text-sm text-gray-500">{{ navDescription }}</div>
</div>
</template>
<a-image
v-if="navBanner"
:src="navBanner"
:preview="false"
class="mb-5 w-full"
/>
<div class="flex flex-wrap items-center gap-3">
<a-input
v-model:value="keywords"
placeholder="搜索本栏目文章"
class="w-72"
allow-clear
@press-enter="doSearch"
/>
<a-button type="primary" :loading="pending" @click="doSearch">搜索</a-button>
<a-button :disabled="pending" @click="refresh">刷新</a-button>
</div>
<a-alert
v-if="loadError"
class="mt-4"
show-icon
type="error"
:message="String(loadError)"
/>
<a-list
class="mt-4"
:data-source="list"
:loading="pending"
item-layout="vertical"
>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #title>
<NuxtLink
v-if="item?.articleId"
class="text-blue-600 hover:underline"
:to="`/item/${item.articleId}`"
>
{{ item.title }}
</NuxtLink>
<span v-else>{{ item?.title }}</span>
</template>
<template #description>
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500">
<span v-if="item?.createTime">发布时间{{ item.createTime }}</span>
<span v-if="item?.author">作者{{ item.author }}</span>
<span v-if="item?.source">来源{{ item.source }}</span>
<span v-if="typeof item?.actualViews === 'number'">阅读{{ item.actualViews }}</span>
</div>
</template>
</a-list-item-meta>
<div class="grid grid-cols-12 gap-4">
<div :class="item?.image ? 'col-span-12 md:col-span-9' : 'col-span-12'">
<div v-if="item?.overview" class="text-gray-700">
{{ item.overview }}
</div>
</div>
<div v-if="item?.image" class="col-span-12 md:col-span-3">
<a-image :src="item.image" :preview="false" class="w-full" />
</div>
</div>
</a-list-item>
</template>
</a-list>
<div class="mt-4 flex items-center justify-end">
<a-pagination
:current="page"
:page-size="limit"
:total="total"
show-size-changer
:page-size-options="['10', '20', '50', '100']"
@change="onPageChange"
@show-size-change="onPageSizeChange"
/>
</div>
</a-card>
</div>
</main>
</template>
<script setup lang="ts">
import { pageCmsArticle } from '@/api/cms/cmsArticle'
import { getCmsNavigation } from '@/api/cms/cmsNavigation'
import { usePageSeo } from '@/composables/usePageSeo'
const route = useRoute()
const navigationId = computed(() => {
const raw = route.params.id
const val = Array.isArray(raw) ? raw[0] : raw
const n = Number(val)
return Number.isFinite(n) ? n : NaN
})
const page = ref(1)
const limit = ref(10)
const keywords = ref('')
watch(
() => route.query.keywords,
(val) => {
const next = Array.isArray(val) ? val[0] : val
if (typeof next === 'string') keywords.value = next.trim()
},
{ immediate: true }
)
const {
data,
pending,
error: loadError,
refresh
} = useAsyncData(
() => `cms-navigation-article-page-${navigationId.value}-${page.value}-${limit.value}-${keywords.value}`,
async () => {
if (!Number.isFinite(navigationId.value)) throw new Error('无效的栏目ID')
const [nav, articles] = await Promise.all([
getCmsNavigation(navigationId.value),
pageCmsArticle({
page: page.value,
limit: limit.value,
keywords: keywords.value || undefined,
status: 0,
// Some backends use categoryId, some use navigationId; send both.
categoryId: navigationId.value,
navigationId: navigationId.value
})
])
return { nav, articles }
},
{ watch: [navigationId] }
)
const navTitle = computed(() => data.value?.nav?.title?.trim() || `栏目 ${navigationId.value}`)
const navDescription = computed(() => data.value?.nav?.comments?.trim() || '')
const navBanner = computed(() => (typeof data.value?.nav?.banner === 'string' ? data.value?.nav?.banner.trim() : ''))
const list = computed(() => data.value?.articles?.list ?? [])
const total = computed(() => data.value?.articles?.count ?? 0)
function doSearch() {
page.value = 1
refresh()
}
function onPageChange(nextPage: number) {
page.value = nextPage
refresh()
}
function onPageSizeChange(_current: number, nextSize: number) {
limit.value = nextSize
page.value = 1
refresh()
}
watchEffect(() => {
if (!Number.isFinite(navigationId.value)) return
usePageSeo({
title: navTitle.value,
description: navDescription.value || navTitle.value,
path: route.fullPath
})
})
</script>

View File

@@ -1,158 +0,0 @@
<template>
<main class="min-h-screen bg-gray-50 p-8">
<div class="mx-auto max-w-5xl space-y-6">
<a-card title="文章列表 (pageCmsArticle)" class="shadow-sm">
<div class="flex flex-wrap items-center gap-3">
<a-input-password
v-model:value="token"
placeholder="Authorization (AccessToken)"
class="w-96"
/>
<a-button :disabled="pending" @click="applyToken">设置Token</a-button>
<a-button :disabled="pending" danger @click="clearToken">清除Token</a-button>
<a-input
v-model:value="keywords"
placeholder="关键词 keywords"
class="w-72"
@press-enter="doSearch"
/>
<a-button type="primary" :loading="pending" @click="doSearch">查询</a-button>
<a-button :disabled="pending" @click="refresh">刷新</a-button>
<div class="text-sm text-gray-500">
TenantId: {{ tenantId }}
</div>
</div>
<a-alert
v-if="error"
class="mt-4"
show-icon
type="error"
:message="String(error)"
/>
<a-table
class="mt-4"
:data-source="list"
:loading="pending"
:pagination="false"
row-key="articleId"
size="middle"
>
<a-table-column title="ID" data-index="articleId" width="90" />
<a-table-column title="标题" data-index="title">
<template #default="{ record }">
<NuxtLink
v-if="record?.articleId"
class="text-blue-600 hover:underline"
:to="`/item/${record.articleId}`"
>
{{ record.title }}
</NuxtLink>
<span v-else>{{ record.title }}</span>
</template>
</a-table-column>
<a-table-column title="编号" data-index="code" width="220" />
<a-table-column title="栏目" data-index="categoryName" width="160" />
<a-table-column title="创建时间" data-index="createTime" width="180" />
<a-table-column title="操作" key="action" width="120">
<template #default="{ record }">
<a-button
size="small"
type="link"
:disabled="!record?.articleId"
@click="record?.articleId && navigateTo(`/item/${record.articleId}`)"
>
查看
</a-button>
</template>
</a-table-column>
</a-table>
<div class="mt-4 flex items-center justify-end">
<a-pagination
:current="page"
:page-size="limit"
:total="total"
show-size-changer
:page-size-options="['10', '20', '50', '100']"
@change="onPageChange"
@show-size-change="onPageSizeChange"
/>
</div>
</a-card>
</div>
</main>
</template>
<script setup lang="ts">
import { pageCmsArticle } from '@/api/cms/cmsArticle/index'
import { getToken, removeToken, setToken } from '@/utils/token-util'
const route = useRoute()
const config = useRuntimeConfig()
const tenantId = computed(() => String(config.public.tenantId))
const page = ref(1)
const limit = ref(10)
const keywords = ref('')
const token = ref('')
onMounted(() => {
token.value = getToken()
})
watch(
() => route.query.keywords,
(val) => {
const next = Array.isArray(val) ? val[0] : val
if (typeof next === 'string' && next.trim() && next.trim() !== keywords.value.trim()) {
keywords.value = next.trim()
page.value = 1
refresh()
}
},
{ immediate: true }
)
const { data, pending, error, refresh } = useAsyncData(
'cms-article-page',
() =>
pageCmsArticle({
page: page.value,
limit: limit.value,
keywords: keywords.value || undefined
}),
{ server: false }
)
const list = computed(() => data.value?.list ?? [])
const total = computed(() => data.value?.count ?? 0)
function applyToken() {
setToken(token.value, true)
refresh()
}
function clearToken() {
removeToken()
token.value = ''
refresh()
}
function doSearch() {
page.value = 1
refresh()
}
function onPageChange(nextPage: number) {
page.value = nextPage
refresh()
}
function onPageSizeChange(_current: number, nextSize: number) {
limit.value = nextSize
page.value = 1
refresh()
}
</script>

View File

@@ -1,91 +0,0 @@
<template>
<div class="mx-auto max-w-screen-xl px-4 py-12">
<a-typography-title :level="1" class="!mb-2">部署方案</a-typography-title>
<a-typography-paragraph class="!text-gray-600 !mb-8">
支持 SaaS私有化与混合部署针对安全合规/数据隔离/运维可控等需求提供交付物清单与验收流程
</a-typography-paragraph>
<a-alert
class="mb-6"
type="info"
show-icon
message="支持私有化部署:提供部署文档、初始化脚本、升级/回滚建议与验收清单。"
/>
<a-table :columns="columns" :data-source="rows" :pagination="false" row-key="key" />
<a-row class="mt-10" :gutter="[16, 16]">
<a-col :xs="24" :md="12">
<a-card title="私有化交付清单(示例)">
<a-list :data-source="deliverables" size="small" bordered>
<template #renderItem="{ item }">
<a-list-item>{{ item }}</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<a-col :xs="24" :md="12">
<a-card title="上线与升级策略">
<a-collapse>
<a-collapse-panel key="1" header="升级方式">
<div class="text-gray-600">
支持版本升级与兼容性说明建议灰度升级并保留回滚方案
</div>
</a-collapse-panel>
<a-collapse-panel key="2" header="数据安全">
<div class="text-gray-600">
提供租户隔离权限审计数据备份/恢复建议可对接客户既有安全体系
</div>
</a-collapse-panel>
<a-collapse-panel key="3" header="高可用建议">
<div class="text-gray-600">
提供多实例部署负载均衡与健康检查建议根据业务量规划资源与扩容策略
</div>
</a-collapse-panel>
</a-collapse>
</a-card>
</a-col>
</a-row>
<div class="mt-10">
<a-space>
<a-button @click="navigateTo('/flow')">查看开通流程</a-button>
<a-button type="primary" @click="navigateTo('/contact')">获取部署方案</a-button>
</a-space>
</div>
</div>
</template>
<script setup lang="ts">
import { usePageSeo } from '@/composables/usePageSeo'
usePageSeo({
title: '部署方案 - SaaS / 私有化 / 混合部署',
description: '支持 SaaS、私有化与混合部署提供交付物清单与验收流程满足安全合规与运维可控需求。',
path: '/deploy'
})
const columns = [
{ title: '对比项', dataIndex: 'name' },
{ title: 'SaaS', dataIndex: 'saas' },
{ title: '私有化', dataIndex: 'private' },
{ title: '混合', dataIndex: 'hybrid' }
]
const rows = [
{ key: 'k1', name: '交付速度', saas: '最快', private: '中', hybrid: '中' },
{ key: 'k2', name: '数据与合规', saas: '标准', private: '最高可控', hybrid: '可定制' },
{ key: 'k3', name: '运维成本', saas: '最低', private: '客户自运维', hybrid: '可分担' },
{ key: 'k4', name: '扩展能力', saas: '强', private: '强', hybrid: '强' },
{ key: 'k5', name: '适用场景', saas: '快速试用/中小团队', private: '政企/强合规', hybrid: '集团/多系统' }
]
const deliverables = [
'部署包/镜像(示例)',
'部署与运维文档(示例)',
'初始化脚本与默认配置(示例)',
'验收清单与检查项(示例)',
'升级/回滚建议(示例)'
]
</script>

View File

@@ -1,71 +0,0 @@
<template>
<div class="mx-auto max-w-screen-xl px-4 py-12">
<a-typography-title :level="1" class="!mb-2">业务流程开通链路</a-typography-title>
<a-typography-paragraph class="!text-gray-600 !mb-8">
面向产品售卖 + 自动交付的业务模型客户选择产品并支付后平台自动创建租户初始化模块与数据并完成交付上线
</a-typography-paragraph>
<a-row :gutter="[24, 24]">
<a-col :xs="24" :lg="12">
<a-card title="对外流程(客户视角)">
<a-steps direction="vertical" :current="-1">
<a-step title="选择产品/套餐" description="选择产品矩阵中的产品与套餐,按需加购模板/插件" />
<a-step title="支付下单" description="支付成功后触发开通编排任务" />
<a-step title="收到交付入口" description="获取管理员账号、访问地址与基础指引" />
<a-step title="开始配置与运营" description="基于模板与默认配置快速上线,按需启用插件" />
</a-steps>
</a-card>
</a-col>
<a-col :xs="24" :lg="12">
<a-card title="平台内部自动化(系统视角)">
<a-timeline>
<a-timeline-item>订单校验产品/套餐/加购项与授权生成</a-timeline-item>
<a-timeline-item>创建租户租户信息域名/应用信息管理员生成</a-timeline-item>
<a-timeline-item>模块装配按所购产品组合模块并初始化菜单/权限</a-timeline-item>
<a-timeline-item>数据初始化基础字典默认配置可选演示数据</a-timeline-item>
<a-timeline-item>交付通知发送入口账号初始化结果与下一步指引</a-timeline-item>
</a-timeline>
</a-card>
</a-col>
</a-row>
<a-card class="mt-10" title="常见问题">
<a-collapse>
<a-collapse-panel key="1" header="如何支持私有化部署?">
<div class="text-gray-600">
私有化交付可提供部署包/镜像部署文档初始化脚本与验收清单按客户环境对接域名存储日志与监控体系
</div>
</a-collapse-panel>
<a-collapse-panel key="2" header="模板/插件购买后如何生效?">
<div class="text-gray-600">
支付成功后生成授权并在租户侧自动装配模板应用到站点与配置插件完成安装/启用与默认配置写入
</div>
</a-collapse-panel>
<a-collapse-panel key="3" header="初始化哪些数据?">
<div class="text-gray-600">
可按产品套餐选择基础字典默认配置菜单与权限可选演示数据/示例内容便于开通即验收
</div>
</a-collapse-panel>
</a-collapse>
</a-card>
<div class="mt-10">
<a-space>
<a-button @click="navigateTo('/products')">选择产品</a-button>
<a-button type="primary" @click="navigateTo('/contact')">咨询开通与交付</a-button>
</a-space>
</div>
</div>
</template>
<script setup lang="ts">
import { usePageSeo } from '@/composables/usePageSeo'
usePageSeo({
title: '开通流程 - 选品支付 / 创建租户 / 初始化模块与数据',
description:
'客户选择产品并支付后,平台自动创建租户、装配模块、初始化菜单权限与基础数据,并完成交付上线;支持 SaaS 与私有化交付。',
path: '/flow'
})
</script>

View File

@@ -1,37 +1,95 @@
<template>
<main class="home">
<section class="mx-auto max-w-screen-xl px-4 py-6">
<section class="mx-auto max-w-screen-xl px-4 py-8">
<div class="grid grid-cols-12 gap-6">
<div class="col-span-12 lg:col-span-7">
<div class="panel">
<a-image :src="featured.image" :preview="false" class="featured-image" />
<div class="p-4">
<div class="featured-title">{{ featured.title }}</div>
<div class="featured-meta">{{ featured.date }}</div>
<div class="panel hero">
<div class="hero-inner">
<!-- <div class="hero-badge">OFFICIAL</div>-->
<!-- <div class="hero-title">{{ COMPANY.projectName }}</div>-->
<!-- <div class="hero-sub">生物基材料技术研发 · 技术服务 · 食品与农产品流通</div>-->
<!-- <div class="mt-6 flex flex-wrap gap-3">-->
<!-- <a-button type="primary" size="large" @click="navigateTo('/contact')">-->
<!-- <template #icon><PhoneOutlined /></template>-->
<!-- 合作咨询-->
<!-- </a-button>-->
<!-- <a-button size="large" @click="scrollToCompany">-->
<!-- <template #icon><IdcardOutlined /></template>-->
<!-- 工商信息-->
<!-- </a-button>-->
<!-- </div>-->
<!-- <div class="mt-6 flex flex-wrap gap-2">-->
<!-- <a-tag v-for="t in COMPANY.tags" :key="t" color="green">{{ t }}</a-tag>-->
<!-- </div>-->
</div>
</div>
</div>
<div class="col-span-12 lg:col-span-5">
<div class="panel notice">
<div class="notice-head">
<div class="notice-title">
<NotificationOutlined />
公告
</div>
<a class="notice-more" href="#" @click.prevent>更多</a>
<div class="panel quick">
<div class="login-hero quick-hero">
<div class="login-hero-title">桂乐淘 · 官方网站</div>
<div class="login-hero-sub">QUALITY · SERVICE · COMPLIANCE</div>
</div>
<div class="notice-list">
<a
v-for="n in notices"
:key="n.title"
class="notice-item"
href="#"
@click.prevent
>
<div class="notice-item-title">{{ n.title }}</div>
<div class="notice-item-desc">{{ n.desc }}</div>
</a>
<div class="p-4">
<a-space direction="vertical" class="w-full" size="middle">
<a-button type="primary" block size="large" @click="navigateTo('/products')">
<template #icon><FileTextOutlined /></template>
经营范围与资质信息
</a-button>
<a-button block size="large" @click="navigateTo('/contact')">
<template #icon><PhoneOutlined /></template>
合作咨询 / 联系我们
</a-button>
<div class="flex items-start justify-between gap-3 text-sm text-gray-500">
<span class="leading-6">地址{{ COMPANY.address }}</span>
<a href="#" class="text-green-600" @click.prevent="scrollToCompany">查看</a>
</div>
</a-space>
</div>
</div>
</div>
<div class="col-span-12 lg:col-span-5 hidden">
<div id="company" class="panel company">
<div class="panel-head">
<div class="panel-title">
<IdcardOutlined />
工商信息
</div>
<a class="panel-more" href="#" @click.prevent="navigateTo('/products')">详情</a>
</div>
<div class="p-4">
<a-descriptions bordered size="small" :column="1">
<a-descriptions-item label="项目名称">{{ COMPANY.projectName }}</a-descriptions-item>
<a-descriptions-item label="注册地址">{{ COMPANY.address }}</a-descriptions-item>
</a-descriptions>
<a-divider class="!my-4" />
<a-collapse ghost>
<a-collapse-panel key="general" header="一般项目(经营范围)">
<a-list size="small" :data-source="generalScopeItems">
<template #renderItem="{ item }">
<a-list-item class="scope-item">{{ item }}</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel key="licensed" header="许可项目">
<a-list size="small" :data-source="licensedScopeItems">
<template #renderItem="{ item }">
<a-list-item class="scope-item">{{ item }}</a-list-item>
</template>
</a-list>
</a-collapse-panel>
</a-collapse>
<a-alert
class="mt-4"
type="info"
show-icon
message="经营范围展示以工商登记为准(涉及许可项目请以许可文件为准)。"
/>
</div>
</div>
</div>
@@ -42,28 +100,28 @@
<div class="panel">
<div class="section-pill">
<span class="pill-left">
<FileTextOutlined />
申报指南
<AppstoreOutlined />
业务板块
</span>
<span class="pill-right">Declaration Guide</span>
<span class="pill-right">SERVICES</span>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<a-card
v-for="g in guides"
:key="g.title"
class="guide-card"
v-for="s in services"
:key="s.title"
class="guide-card service-card"
:bordered="true"
hoverable
@click="navigateTo(g.to)"
@click="navigateTo('/products')"
>
<a-space>
<a-avatar :size="44" class="guide-icon">
<component :is="g.icon" />
<a-avatar :size="44" class="guide-icon service-icon">
<component :is="s.icon" />
</a-avatar>
<div>
<div class="guide-title">{{ g.title }}</div>
<div class="guide-desc">{{ g.desc }}</div>
<div class="guide-title">{{ s.title }}</div>
<div class="guide-desc">{{ s.desc }}</div>
</div>
</a-space>
</a-card>
@@ -71,44 +129,20 @@
</div>
</div>
<div class="col-span-12 lg:col-span-5">
<div class="panel login">
<div class="login-hero">
<div class="login-hero-title">破产重整债权申报系统</div>
<div class="login-hero-sub">SUBMIT REQUIREMENTS</div>
</div>
<div class="p-4">
<a-space direction="vertical" class="w-full" size="middle">
<a-button type="primary" block size="large" @click="navigateTo('/login')">
<template #icon><LoginOutlined /></template>
用户登录申报
</a-button>
<a-button block size="large" @click="navigateTo('/create-app')">
<template #icon><UserAddOutlined /></template>
新用户注册申报
</a-button>
<div class="flex items-center justify-between text-sm text-gray-500">
<span>申报审核系统</span>
<a href="#" class="text-red-600" @click.prevent>忘记密码</a>
</div>
</a-space>
</div>
</div>
</div>
</div>
</section>
<section class="banner">
<div class="mx-auto max-w-screen-xl px-4 py-10">
<div class="banner-title">致力于专业破产事务服务</div>
<div class="banner-title">以合规经营与品质服务为核心</div>
</div>
</section>
<section class="mx-auto max-w-screen-xl px-4 py-10">
<div class="section-title">
<div class="section-title-main">新闻资讯</div>
<div class="section-title-sub">NEWS INFORMATION</div>
<div class="section-title-main">资讯与公告</div>
<div class="section-title-sub">NEWS & UPDATES</div>
</div>
<div class="mt-6 grid grid-cols-12 gap-6">
@@ -135,23 +169,20 @@
<div class="mt-10">
<div class="section-title">
<div class="section-title-main">典型案例</div>
<div class="section-title-sub">CLASSIC CASE</div>
<div class="section-title-main">资质与合作</div>
<div class="section-title-sub">COMPLIANCE</div>
</div>
<div class="mt-6 grid grid-cols-12 gap-6">
<a-card
v-for="c in cases"
v-for="c in compliance"
:key="c.title"
hoverable
class="case-card col-span-12 sm:col-span-6 lg:col-span-3"
@click="navigateTo(c.to)"
>
<template #cover>
<a-image :src="c.image" :preview="false" class="case-image" />
</template>
<a-typography-title :level="5" class="!mb-2 case-title">{{ c.title }}</a-typography-title>
<div class="case-meta">{{ c.date }}</div>
<div class="case-desc">{{ c.desc }}</div>
</a-card>
</div>
</div>
@@ -161,110 +192,119 @@
<script setup lang="ts">
import {
AppstoreOutlined,
FileTextOutlined,
LoginOutlined,
NotificationOutlined,
ProfileOutlined,
IdcardOutlined,
PhoneOutlined,
SafetyCertificateOutlined,
UserAddOutlined
ShopOutlined
} from '@ant-design/icons-vue'
import { usePageSeo } from '@/composables/usePageSeo'
import { COMPANY } from '@/config/company'
const featured = {
image:
'https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1400&q=80',
title: '行于思清算公司受指定担任广西民族包装有限公司管理人',
date: '2023-10-11'
usePageSeo({
title: '首页',
description:
'桂乐淘:生物基材料技术研发、技术服务与食品/农产品流通服务。提供经营范围与资质信息查询及合作咨询入口。',
path: '/'
})
function splitScope(text: string) {
return text
.split(/[;]+/g)
.map((s) => s.trim())
.filter(Boolean)
.map((s) => s.replace(/[。.]$/, '').trim())
}
const notices = [
{
title: '关于公开招募破产清算管理主体框架及维修施工单位的公告',
desc: '南宁市中级人民法院根据有关规定...'
},
{
title: '钦州市王明年丰全仓储有限公司重整投资人招募公告',
desc: '钦州市王明年丰全仓储有限公司...'
},
{
title: '钦州市王明年丰全仓储有限公司 预重整债权申报公告',
desc: '债权申报相关材料及申报途径...'
},
{
title: '钦州市王明年丰全仓储有限公司 预重整债权申报公告(补充)',
desc: '补充说明及常见问题...'
}
]
const generalScopeItems = computed(() => splitScope(COMPANY.scope.general))
const licensedScopeItems = computed(() => splitScope(COMPANY.scope.licensed))
const guides = [
{ title: '线上实名认证指南', desc: '快速完成实名认证', to: '/flow', icon: ProfileOutlined },
{ title: '债权人提交材料指南', desc: '材料清单与提交规范', to: '/articles', icon: FileTextOutlined },
{ title: '债权人网上申报指南', desc: '在线申报流程说明', to: '/login', icon: SafetyCertificateOutlined },
{ title: '公司自主清算债权登记材料范本', desc: '模板下载与填写说明', to: '/deploy', icon: FileTextOutlined }
const services = [
{
title: '生物基材料技术研发',
desc: '面向应用场景开展研发与技术支持',
icon: SafetyCertificateOutlined
},
{
title: '技术服务与技术推广',
desc: '技术开发/咨询/交流/转让/推广',
icon: AppstoreOutlined
},
{
title: '食品销售(预包装)',
desc: '含保健食品(预包装)销售等',
icon: ShopOutlined
},
{
title: '农产品与生鲜流通',
desc: '鲜肉/水产/蔬果/蛋类等零售与批发',
icon: FileTextOutlined
}
]
const columns = [
{
title: '新闻动态',
title: '公司动态',
items: [
'标题新闻标题新闻标题新闻标题新闻标题...',
'标题新闻标题新闻标题新闻标题新闻标题...',
'标题新闻标题新闻标题新闻标题新闻标题...',
'标题新闻标题新闻标题新闻标题新闻标题...',
'标题新闻标题新闻标题新闻标题新闻标题...'
'桂乐淘官方网站上线公告',
'业务范围与资质信息已更新',
'合作咨询:欢迎留言,我们将尽快联系',
'合规提示:涉及许可项目以许可文件为准',
'更多动态敬请期待...'
]
},
{
title: '法律法规',
title: '行业资讯',
items: [
'标题新闻标题新闻标题新闻标题新闻标题...',
'标题新闻标题新闻标题新闻标题新闻标题...',
'标题新闻标题新闻标题新闻标题新闻标题...',
'标题新闻标题新闻标题新闻标题新闻标题...',
'标题新闻标题新闻标题新闻标题新闻标题...'
'生物基材料与绿色低碳趋势速览',
'食品流通与预包装食品合规要点',
'冷链与生鲜品质管理建议',
'更多资讯敬请期待...',
'更多资讯敬请期待...'
]
},
{
title: '权威发布',
title: '合规与公告',
items: [
'标题新闻标题新闻标题新闻标题新闻标题...',
'标题新闻标题新闻标题新闻标题新闻标题...',
'标题新闻标题新闻标题新闻标题新闻标题...',
'标题新闻标题新闻标题新闻标题新闻标题...',
'标题新闻标题新闻标题新闻标题新闻标题...'
'一般项目:凭营业执照依法自主开展经营活动',
'许可项目:依法须经批准的项目,经批准后方可开展',
'具体经营项目以相关部门批准文件或许可证件为准',
'更多公告敬请期待...',
'更多公告敬请期待...'
]
}
]
const cases = [
const compliance = [
{
title: '广西南院审理的印象刘三姐重整案...',
date: '2023-10-17',
to: '/articles',
image:
'https://images.unsplash.com/photo-1450101499163-c8848c66ca85?auto=format&fit=crop&w=900&q=80'
title: '经营范围',
desc: '一般项目/许可项目明细与说明',
to: '/products'
},
{
title: '广西南院审理的印象刘三姐重整案...',
date: '2023-10-17',
to: '/articles',
image:
'https://images.unsplash.com/photo-1520607162513-77705c0f0d4a?auto=format&fit=crop&w=900&q=80'
title: '注册地址',
desc: '南宁市江南区国凯大道东13号神冠胶原智库项目加工厂房',
to: '/products'
},
{
title: '广西南院审理的印象刘三姐重整案...',
date: '2023-10-17',
to: '/articles',
image:
'https://images.unsplash.com/photo-1521790797524-b2497295b8a0?auto=format&fit=crop&w=900&q=80'
title: '合作咨询',
desc: '提交需求与合作意向,我们尽快联系',
to: '/contact'
},
{
title: '广西南院审理的印象刘三姐重整案...',
date: '2023-10-17',
to: '/articles',
image:
'https://images.unsplash.com/photo-1554224155-6726b3ff858f?auto=format&fit=crop&w=900&q=80'
title: '更多内容',
desc: '后续可接入资讯、产品与资质展示',
to: '/contact'
}
]
function scrollToCompany() {
if (!import.meta.client) return
const el = document.querySelector('#company')
if (!el) return
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
</script>
<style scoped>
@@ -280,76 +320,78 @@ const cases = [
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
}
.featured-image :deep(img) {
width: 100%;
height: 360px;
object-fit: cover;
.hero {
min-height: 360px;
background: url('https://oss.wsdns.cn/20260115/75690dea8f064ceda03246b198a7d710.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.featured-title {
font-size: 16px;
font-weight: 700;
color: rgba(0, 0, 0, 0.88);
.hero-inner {
height: 100%;
padding: 28px;
display: flex;
flex-direction: column;
justify-content: center;
}
.featured-meta {
margin-top: 6px;
.hero-badge {
width: fit-content;
padding: 4px 10px;
border-radius: 9999px;
background: rgba(22, 163, 74, 0.12);
color: rgba(21, 128, 61, 0.95);
font-weight: 800;
letter-spacing: 0.12em;
font-size: 12px;
color: rgba(0, 0, 0, 0.5);
}
.notice {
.hero-title {
margin-top: 14px;
font-size: 42px;
line-height: 1.1;
font-weight: 900;
color: rgba(0, 0, 0, 0.9);
}
.hero-sub {
margin-top: 12px;
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
line-height: 1.7;
}
.company {
height: 100%;
}
.notice-head {
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
background: linear-gradient(90deg, #c30000, #e11d48);
background: linear-gradient(90deg, #16a34a, #22c55e);
color: #fff;
}
.notice-title {
.panel-title {
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 800;
}
.notice-more {
.panel-more {
color: rgba(255, 255, 255, 0.9);
font-size: 12px;
}
.notice-list {
padding: 10px 14px 14px;
}
.notice-item {
display: block;
padding: 10px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
text-decoration: none;
}
.notice-item:last-child {
border-bottom: 0;
}
.notice-item-title {
font-size: 13px;
font-weight: 700;
color: rgba(0, 0, 0, 0.85);
line-height: 1.35;
}
.notice-item-desc {
margin-top: 4px;
font-size: 12px;
color: rgba(0, 0, 0, 0.55);
line-height: 1.4;
.scope-item {
padding: 6px 0;
color: rgba(0, 0, 0, 0.75);
line-height: 1.6;
}
.section-pill {
@@ -358,7 +400,7 @@ const cases = [
justify-content: space-between;
padding: 10px 14px;
border-radius: 9999px;
background: linear-gradient(90deg, #c30000, #e11d48);
background: linear-gradient(90deg, #16a34a, #22c55e);
color: #fff;
font-weight: 800;
}
@@ -380,8 +422,13 @@ const cases = [
}
.guide-icon {
background: rgba(195, 0, 0, 0.1);
color: #c30000;
background: rgba(22, 163, 74, 0.12);
color: #16a34a;
}
.service-icon {
background: rgba(22, 163, 74, 0.12);
color: #16a34a;
}
.guide-title {
@@ -400,7 +447,7 @@ const cases = [
padding: 18px 16px;
background:
radial-gradient(circle at 20% 20%, rgba(255, 255, 255, 0.22), transparent 55%),
linear-gradient(90deg, #c30000, #e11d48);
linear-gradient(90deg, #16a34a, #22c55e);
color: #fff;
}
@@ -418,7 +465,7 @@ const cases = [
.banner {
background:
radial-gradient(circle at 20% 30%, rgba(195, 0, 0, 0.18), transparent 55%),
radial-gradient(circle at 20% 30%, rgba(22, 163, 74, 0.18), transparent 55%),
linear-gradient(180deg, #ffffff, #f8fafc);
border-top: 1px solid rgba(0, 0, 0, 0.06);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
@@ -428,7 +475,7 @@ const cases = [
text-align: center;
font-size: 28px;
font-weight: 900;
color: #c30000;
color: #15803d;
}
.section-title {
@@ -463,7 +510,7 @@ const cases = [
.column-more {
font-size: 12px;
color: rgba(195, 0, 0, 0.95);
color: rgba(21, 128, 61, 0.95);
text-decoration: none;
}
@@ -489,19 +536,23 @@ const cases = [
overflow: hidden;
}
.case-image :deep(img) {
width: 100%;
height: 160px;
object-fit: cover;
}
.case-title {
color: rgba(195, 0, 0, 0.95);
color: rgba(21, 128, 61, 0.95);
}
.case-meta {
.case-desc {
font-size: 12px;
color: rgba(0, 0, 0, 0.5);
color: rgba(0, 0, 0, 0.55);
line-height: 1.6;
}
@media (max-width: 640px) {
.hero-inner {
padding: 22px;
}
.hero-title {
font-size: 34px;
}
}
</style>

View File

@@ -1,125 +0,0 @@
<template>
<main class="min-h-screen bg-gray-50 px-4 py-8">
<div class="mx-auto max-w-screen-lg space-y-6">
<a-breadcrumb>
<a-breadcrumb-item>
<NuxtLink to="/">首页</NuxtLink>
</a-breadcrumb-item>
<a-breadcrumb-item>
<NuxtLink to="/articles">文章</NuxtLink>
</a-breadcrumb-item>
<a-breadcrumb-item v-if="article?.categoryId">
<NuxtLink :to="`/article/${article.categoryId}`">
{{ article.categoryName || '栏目' }}
</NuxtLink>
</a-breadcrumb-item>
<a-breadcrumb-item>{{ articleTitle }}</a-breadcrumb-item>
</a-breadcrumb>
<a-skeleton v-if="pending" active :paragraph="{ rows: 10 }" />
<a-result
v-else-if="loadError"
status="error"
title="文章加载失败"
:sub-title="String(loadError)"
/>
<template v-else>
<a-card class="shadow-sm">
<template #title>
<div class="space-y-2">
<div class="text-2xl font-semibold text-gray-900">
{{ articleTitle }}
</div>
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500">
<span v-if="article?.categoryName">栏目{{ article.categoryName }}</span>
<span v-if="article?.author">作者{{ article.author }}</span>
<span v-if="article?.createTime">发布时间{{ article.createTime }}</span>
<span v-if="article?.source">来源{{ article.source }}</span>
</div>
</div>
</template>
<a-alert
v-if="articleOverview"
type="info"
show-icon
class="mb-5"
:message="articleOverview"
/>
<a-image
v-if="articleCover"
:src="articleCover"
:preview="false"
class="mb-5 w-full"
/>
<RichText :content="articleContent" />
</a-card>
</template>
</div>
</main>
</template>
<script setup lang="ts">
import { getCmsArticle } from '@/api/cms/cmsArticle'
import { listCmsArticleContent } from '@/api/cms/cmsArticleContent'
import RichText from '@/components/RichText.vue'
import { usePageSeo } from '@/composables/usePageSeo'
import type { CmsArticle } from '@/api/cms/cmsArticle/model'
const route = useRoute()
const articleId = computed(() => {
const raw = route.params.id
const val = Array.isArray(raw) ? raw[0] : raw
const n = Number(val)
return Number.isFinite(n) ? n : NaN
})
async function loadArticleDetail(id: number): Promise<{ article: CmsArticle; content: string }> {
const article = await getCmsArticle(id)
// Prefer content field, fallback to detail/overview if API differs by model.
let content = String(article.content || article.detail || '').trim()
if (!content) {
try {
const list = await listCmsArticleContent({ articleId: id, limit: 1 })
const hit = Array.isArray(list) ? list[0] : null
if (hit?.content) content = String(hit.content).trim()
} catch {
// ignore content enrichment failures; still render metadata
}
}
return { article, content }
}
const { data, pending, error: loadError } = useAsyncData(
() => `cms-article-item-${articleId.value}`,
async () => {
if (!Number.isFinite(articleId.value)) throw new Error('无效的文章ID')
return loadArticleDetail(articleId.value)
},
{ watch: [articleId] }
)
const article = computed(() => data.value?.article)
const articleTitle = computed(() => article.value?.title?.trim() || `文章 ${articleId.value}`)
const articleOverview = computed(() => article.value?.overview?.trim() || '')
const articleCover = computed(() => (typeof article.value?.image === 'string' ? article.value?.image.trim() : ''))
const articleContent = computed(() => data.value?.content || '')
watchEffect(() => {
if (!Number.isFinite(articleId.value)) return
usePageSeo({
title: articleTitle.value,
description: articleOverview.value || articleTitle.value,
path: route.fullPath
})
})
</script>

View File

@@ -1,87 +0,0 @@
<template>
<div class="mx-auto max-w-screen-xl px-4 py-12">
<a-typography-title :level="1" class="!mb-2">模板 / 插件市场</a-typography-title>
<a-typography-paragraph class="!text-gray-600 !mb-8">
用模板加速交付用插件扩展能力支持购买授权更新与版本管理形成可持续的生态与增值体系
</a-typography-paragraph>
<a-row :gutter="[16, 16]">
<a-col :xs="24" :md="12">
<a-card title="模板:标准化交付">
<a-list :data-source="templates" size="small">
<template #renderItem="{ item }">
<a-list-item>{{ item }}</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<a-col :xs="24" :md="12">
<a-card title="插件:按需扩展与变现">
<a-list :data-source="plugins" size="small">
<template #renderItem="{ item }">
<a-list-item>{{ item }}</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
</a-row>
<a-row class="mt-10" :gutter="[16, 16]">
<a-col :xs="24" :md="12">
<a-card title="购买与授权流程">
<a-steps direction="vertical" :current="-1" size="small">
<a-step title="选择模板/插件" description="支持套餐内置 + 加购" />
<a-step title="下单支付" description="支付成功生成授权与交付记录" />
<a-step title="自动启用" description="为租户装配模板/插件与默认配置" />
<a-step title="更新升级" description="支持版本更新、到期续费与兼容性提示" />
</a-steps>
</a-card>
</a-col>
<a-col :xs="24" :md="12">
<a-card title="生态合作(示例)">
<a-collapse>
<a-collapse-panel key="1" header="插件开发">
<div class="text-gray-600">提供扩展点与规范支持第三方开发并上架</div>
</a-collapse-panel>
<a-collapse-panel key="2" header="模板共建">
<div class="text-gray-600">按行业/场景沉淀模板资产形成标准化交付能力</div>
</a-collapse-panel>
<a-collapse-panel key="3" header="授权与结算">
<div class="text-gray-600">支持授权校验版本管理与结算规则扩展</div>
</a-collapse-panel>
</a-collapse>
</a-card>
</a-col>
</a-row>
<div class="mt-10">
<a-space>
<a-button @click="navigateTo('/products')">看产品矩阵</a-button>
<a-button type="primary" @click="navigateTo('/contact')">咨询合作/上架</a-button>
</a-space>
</div>
</div>
</template>
<script setup lang="ts">
import { usePageSeo } from '@/composables/usePageSeo'
usePageSeo({
title: '模板/插件市场 - 购买/授权/更新/生态合作',
description: '模板加速交付,插件按需扩展;支持购买、授权、更新与版本管理,构建可持续生态与增值体系。',
path: '/market'
})
const templates = [
'行业模板:官网/电商/门户/活动页等',
'一键套用:默认页面 + 配置 + 初始化脚本',
'多版本管理:预览、回滚、迁移与二次编辑'
]
const plugins = [
'能力插件:支付/会员/营销/工单/统计等',
'授权与更新:版本升级、到期续费、兼容性提示',
'生态变现:第三方能力沉淀为可售卖插件'
]
</script>

View File

@@ -1,242 +0,0 @@
<template>
<main class="min-h-screen bg-gray-50 px-4 py-8">
<div class="mx-auto max-w-screen-xl space-y-6">
<a-skeleton v-if="navPending" active :paragraph="{ rows: 8 }" />
<a-result
v-else-if="navError"
status="error"
title="页面加载失败"
:sub-title="String(navError)"
/>
<template v-else>
<a-card class="shadow-sm">
<template #title>
<div class="flex flex-col gap-1">
<div class="text-lg font-semibold text-gray-900">
{{ parentTitle }}
</div>
<div v-if="parentDescription" class="text-sm text-gray-500">
{{ parentDescription }}
</div>
</div>
</template>
<a-image
v-if="parentBanner"
:src="parentBanner"
:preview="false"
class="mb-5 w-full"
/>
<a-layout class="bg-transparent">
<a-layout-sider
v-if="children.length"
theme="light"
:width="240"
class="rounded-xl border border-black/5 bg-white overflow-hidden"
breakpoint="lg"
collapsed-width="0"
>
<a-menu
mode="inline"
:selected-keys="selectedKeys"
@click="onMenuClick"
>
<a-menu-item v-for="n in children" :key="String(n.navigationId)">
{{ n.title || n.label || `栏目 ${n.navigationId}` }}
</a-menu-item>
</a-menu>
</a-layout-sider>
<a-layout-content :class="children.length ? 'pl-0 lg:pl-6 pt-6 lg:pt-0' : ''">
<a-skeleton v-if="contentPending" active :paragraph="{ rows: 10 }" />
<a-result
v-else-if="contentError"
status="error"
title="内容加载失败"
:sub-title="String(contentError)"
/>
<template v-else>
<div class="space-y-2">
<div class="text-2xl font-semibold text-gray-900">
{{ contentTitle }}
</div>
<div class="text-sm text-gray-500">
<span v-if="articleMeta?.createTime">发布时间{{ articleMeta.createTime }}</span>
<span v-if="articleMeta?.author" class="ml-4">作者{{ articleMeta.author }}</span>
<span v-if="articleMeta?.source" class="ml-4">来源{{ articleMeta.source }}</span>
</div>
</div>
<a-alert
v-if="articleMeta?.overview"
type="info"
show-icon
class="my-5"
:message="String(articleMeta.overview)"
/>
<a-image
v-if="articleMeta?.image"
:src="String(articleMeta.image)"
:preview="false"
class="mb-5 w-full"
/>
<RichText :content="contentBody" />
</template>
</a-layout-content>
</a-layout>
</a-card>
</template>
</div>
</main>
</template>
<script setup lang="ts">
import { getCmsArticle } from '@/api/cms/cmsArticle'
import { listCmsArticleContent } from '@/api/cms/cmsArticleContent'
import RichText from '@/components/RichText.vue'
import { usePageSeo } from '@/composables/usePageSeo'
import type { ApiEnvelope } from '~/types/api'
import type { CmsNavigation } from '@/api/cms/cmsNavigation/model'
import type { CmsArticle } from '@/api/cms/cmsArticle/model'
const route = useRoute()
const parentId = computed(() => {
const raw = route.params.id
const val = Array.isArray(raw) ? raw[0] : raw
const n = Number(val)
return Number.isFinite(n) ? n : NaN
})
const selectedNavId = computed(() => {
const raw = route.query.nav
const val = Array.isArray(raw) ? raw[0] : raw
const n = Number(val)
return Number.isFinite(n) ? n : NaN
})
function pickNavigationList(payload: unknown): CmsNavigation[] {
if (Array.isArray(payload)) return payload as CmsNavigation[]
const env = payload as ApiEnvelope<CmsNavigation[]>
if (env && typeof env === 'object' && Array.isArray(env.data)) return env.data
return []
}
const { data: navData, pending: navPending, error: navError } = useAsyncData(
() => `cms-navigation-single-${parentId.value}`,
async () => {
if (!Number.isFinite(parentId.value)) throw new Error('无效的页面ID')
// Single-page type: fetch navigation list by parentId.
const res = await $fetch<unknown>('/api/cms-navigation', {
query: { parentId: String(parentId.value) }
})
const children = pickNavigationList(res)
.filter((n) => (n.hide ?? 0) !== 1)
.slice()
.sort((a, b) => (a.sortNumber ?? 0) - (b.sortNumber ?? 0))
return { children }
},
{ watch: [parentId] }
)
const children = computed<CmsNavigation[]>(() => navData.value?.children ?? [])
const activeNav = computed<CmsNavigation | null>(() => {
const list = children.value
if (!list.length) return null
if (Number.isFinite(selectedNavId.value)) {
const hit = list.find((n) => n.navigationId === selectedNavId.value)
if (hit) return hit
}
return list[0]
})
// Keep URL stable so refresh/back works (default to first child).
if (import.meta.client) {
watchEffect(() => {
if (!children.value.length) return
if (Number.isFinite(selectedNavId.value)) return
const first = children.value[0]?.navigationId
if (!first) return
navigateTo({ path: route.path, query: { ...route.query, nav: String(first) } }, { replace: true })
})
}
const selectedKeys = computed(() => {
const key = activeNav.value?.navigationId
return key ? [String(key)] : []
})
function onMenuClick(info: { key: string }) {
navigateTo({ path: route.path, query: { ...route.query, nav: String(info.key) } })
}
async function loadSinglePageContent(nav: CmsNavigation): Promise<{
title: string
body: string
article?: CmsArticle
}> {
// Common pattern: navigation.itemId binds to an articleId for "single page" content.
const articleId =
(typeof nav.itemId === 'number' && Number.isFinite(nav.itemId) ? nav.itemId : 0) ||
(typeof nav.pageId === 'number' && Number.isFinite(nav.pageId) ? nav.pageId : 0)
if (articleId > 0) {
const article = await getCmsArticle(articleId)
let body = String(article.content || article.detail || '').trim()
if (!body) {
try {
const list = await listCmsArticleContent({ articleId, limit: 1 })
const hit = Array.isArray(list) ? list[0] : null
if (hit?.content) body = String(hit.content).trim()
} catch {
// ignore
}
}
return { title: article.title?.trim() || String(nav.title || '单页'), body, article }
}
// Fallback: show navigation meta as content when no binding exists.
return {
title: String(nav.title || '单页'),
body: String(nav.comments || nav.meta || '').trim()
}
}
const {
data: contentData,
pending: contentPending,
error: contentError
} = useAsyncData(
() => `cms-single-page-content-${parentId.value}-${activeNav.value?.navigationId ?? 'none'}`,
async () => {
if (!activeNav.value) return { title: '', body: '' }
return loadSinglePageContent(activeNav.value)
},
{ watch: [activeNav] }
)
const parentTitle = computed(() => children.value[0]?.parentName?.trim() || `页面 ${parentId.value}`)
const parentDescription = computed(() => '')
const parentBanner = computed(() => '')
const contentTitle = computed(() => contentData.value?.title?.trim() || '')
const contentBody = computed(() => contentData.value?.body || '')
const articleMeta = computed(() => contentData.value?.article)
watchEffect(() => {
if (!Number.isFinite(parentId.value)) return
usePageSeo({
title: contentTitle.value || parentTitle.value,
description:
(articleMeta.value?.overview?.trim() || parentDescription.value || contentTitle.value || parentTitle.value),
path: route.fullPath
})
})
</script>

View File

@@ -1,115 +0,0 @@
<template>
<div class="mx-auto max-w-screen-xl px-4 py-12">
<a-typography-title :level="1" class="!mb-2">平台能力</a-typography-title>
<a-typography-paragraph class="!text-gray-600 !mb-8">
平台定位为 SaaS 软件开发平台支持私有化部署提供模板/插件生态并将下单支付自动开通初始化交付标准化
</a-typography-paragraph>
<a-tabs>
<a-tab-pane key="tenant" tab="多租户与权限">
<a-row :gutter="[16, 16]">
<a-col :xs="24" :md="12">
<a-card title="租户隔离">
<a-typography-paragraph class="!mb-0 !text-gray-600">
租户数据隔离域名/应用信息绑定独立配置与配额策略适配多业务线与多客户运营
</a-typography-paragraph>
</a-card>
</a-col>
<a-col :xs="24" :md="12">
<a-card title="权限体系">
<a-typography-paragraph class="!mb-0 !text-gray-600">
角色/菜单/按钮权限审计与操作日志支持模块级别的权限装配与初始化
</a-typography-paragraph>
</a-card>
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane key="modules" tab="模块化与扩展">
<a-row :gutter="[16, 16]">
<a-col :xs="24" :md="12">
<a-card title="模块组合产品">
<a-typography-paragraph class="!mb-0 !text-gray-600">
将能力拆分为可组合的模块内容商品订单会员营销表单等可按产品套餐选择并初始化
</a-typography-paragraph>
</a-card>
</a-col>
<a-col :xs="24" :md="12">
<a-card title="扩展点与二开">
<a-typography-paragraph class="!mb-0 !text-gray-600">
支持插件化扩展点便于第三方能力集成与二次开发可沉淀为生态插件进行复用与变现
</a-typography-paragraph>
</a-card>
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane key="market" tab="模板/插件生态">
<a-row :gutter="[16, 16]">
<a-col :xs="24" :md="12">
<a-card title="模板市场">
<a-typography-paragraph class="!mb-0 !text-gray-600">
行业模板 + 初始化脚本配套让交付标准化支持多版本预览回滚与迁移
</a-typography-paragraph>
</a-card>
</a-col>
<a-col :xs="24" :md="12">
<a-card title="插件市场">
<a-typography-paragraph class="!mb-0 !text-gray-600">
插件按需购买启用支持授权校验更新与兼容性管理可用于增值服务与生态合作
</a-typography-paragraph>
</a-card>
</a-col>
</a-row>
<div class="mt-6">
<a-button @click="navigateTo('/market')">了解模板/插件市场</a-button>
</div>
</a-tab-pane>
<a-tab-pane key="ops" tab="运维与安全">
<a-row :gutter="[16, 16]">
<a-col :xs="24" :md="12">
<a-card title="可观测与运维">
<a-typography-paragraph class="!mb-0 !text-gray-600">
支持日志/告警/指标等可观测能力接入提供升级策略灰度与回滚建议
</a-typography-paragraph>
</a-card>
</a-col>
<a-col :xs="24" :md="12">
<a-card title="私有化与合规">
<a-typography-paragraph class="!mb-0 !text-gray-600">
支持专有云/本地部署与数据安全策略可按客户要求提供交付物清单与验收流程
</a-typography-paragraph>
</a-card>
</a-col>
</a-row>
</a-tab-pane>
</a-tabs>
<a-card class="mt-10" :bordered="false" style="background: #f6ffed">
<a-row :gutter="[16, 16]" align="middle">
<a-col :xs="24" :md="16">
<a-typography-title :level="3" class="!mb-1">想把你的业务做成可售卖的产品</a-typography-title>
<a-typography-paragraph class="!mb-0 !text-gray-600">
我们可以基于平台能力帮你规划产品套餐开通链路与交付标准
</a-typography-paragraph>
</a-col>
<a-col :xs="24" :md="8" class="flex md:justify-end">
<a-button type="primary" size="large" @click="navigateTo('/contact')">马上联系</a-button>
</a-col>
</a-row>
</a-card>
</div>
</template>
<script setup lang="ts">
import { usePageSeo } from '@/composables/usePageSeo'
usePageSeo({
title: '平台能力 - SaaS 多租户 / 模块化 / 模板&插件生态',
description:
'平台定位为 SaaS 软件开发平台,支持私有化部署与模板/插件生态,标准化“下单支付→自动开通→初始化交付”全链路。',
path: '/platform'
})
</script>

81
app/pages/products.vue Normal file
View File

@@ -0,0 +1,81 @@
<template>
<div class="mx-auto max-w-screen-xl px-4 py-12">
<a-typography-title :level="1" class="!mb-2">经营范围</a-typography-title>
<a-typography-paragraph class="!text-gray-600 !mb-8">
以下信息用于公示与参考最终以工商登记及相关许可文件/许可证件为准
</a-typography-paragraph>
<a-row :gutter="[24, 24]">
<a-col :xs="24" :lg="14">
<a-card title="一般项目(经营范围)">
<a-list size="small" :data-source="generalItems">
<template #renderItem="{ item }">
<a-list-item class="scope-item">{{ item }}</a-list-item>
</template>
</a-list>
</a-card>
<a-card class="mt-6" title="许可项目">
<a-list size="small" :data-source="licensedItems">
<template #renderItem="{ item }">
<a-list-item class="scope-item">{{ item }}</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<a-col :xs="24" :lg="10">
<a-card title="基本信息">
<a-descriptions bordered :column="1" size="small">
<a-descriptions-item label="项目名称">{{ COMPANY.projectName }}</a-descriptions-item>
<a-descriptions-item label="注册地址">{{ COMPANY.address }}</a-descriptions-item>
</a-descriptions>
<a-alert
class="mt-4"
show-icon
type="warning"
message="涉及许可项目的经营活动,请以主管部门批准文件/许可证件为准。"
/>
</a-card>
<a-card class="mt-6" title="快速入口">
<a-space direction="vertical" class="w-full">
<a-button type="primary" block @click="navigateTo('/contact')">合作咨询</a-button>
<a-button block @click="navigateTo('/')">返回首页</a-button>
</a-space>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { usePageSeo } from '@/composables/usePageSeo'
import { COMPANY } from '@/config/company'
usePageSeo({
title: '经营范围',
description: `${COMPANY.projectName} 经营范围公示:一般项目、许可项目及注册地址信息。`,
path: '/products'
})
function splitScope(text: string) {
return text
.split(/[;]+/g)
.map((s) => s.trim())
.filter(Boolean)
.map((s) => s.replace(/[。.]$/, '').trim())
}
const generalItems = computed(() => splitScope(COMPANY.scope.general))
const licensedItems = computed(() => splitScope(COMPANY.scope.licensed))
</script>
<style scoped>
.scope-item {
padding: 6px 0;
color: rgba(0, 0, 0, 0.75);
line-height: 1.6;
}
</style>