初始化

This commit is contained in:
2026-07-18 05:57:07 +08:00
parent 945b6d3d70
commit 6d42848322
50 changed files with 2937 additions and 2266 deletions

View File

@@ -0,0 +1,12 @@
---
description: 萧康云医项目全局编码规则
alwaysApply: true
---
# 全局规则
- 代码、包、方法、结构体、接口、关键业务分支、复杂判断、关键配置均需要编写清晰的中文注释。
- 注释要说明业务意图、输入输出、边界条件和注意事项,避免只重复代码字面含义。
- 模块职责要清晰,避免跨层调用、重复实现和无关逻辑混入。
- 新增或修改功能时,优先遵循项目已有目录结构、命名方式、封装习惯和错误处理方式。
- 公共能力应沉淀为可复用模块或组件,避免在多个页面、控制器或服务中复制粘贴。

View File

@@ -0,0 +1,17 @@
---
description: 萧康云医官网前端设计与实现规则
globs: xk-of-view/**/*,**/*.html,**/*.css,**/*.scss,**/*.less,**/*.vue,**/*.tsx,**/*.ts,**/*.jsx,**/*.js
alwaysApply: false
---
# 前端规则
- 项目名称为“萧康云医官网”,定位为面向诊所赋能业务的 SaaS 平台。
- 产品端形态包含小程序患者端、医生端、门店老板端、推广员端,以及 PC 端管理或官网相关页面。
- 页面设计要体现 SaaS 平台的专业、清晰、高效,优先服务信息扫描、业务转化和多角色理解。
- 必须注意多端适配,兼容移动端、平板端和桌面端,不允许出现文字溢出、布局重叠、关键按钮不可触达等问题。
- 必须提供暗色模式适配,颜色、阴影、边框、图标、图表和状态提示都要在明暗主题下保持可读。
- 采用模块化、组件化设计,页面由清晰的业务模块和可复用 UI 组件组成。
- 组件应保持职责单一,状态、事件、属性和插槽设计清楚,避免单个组件承担过多页面逻辑。
- 表单、列表、筛选、弹窗、空状态、加载态、错误态、成功态等常见状态要完整设计。
- 视觉风格要克制、现代、可信,适合医疗和诊所经营场景;避免过度营销化、装饰化或单一色系堆叠。

View File

@@ -0,0 +1,17 @@
---
description: 萧康云医后端 GoFrame GF 分层开发规则
globs: **/*.go,api/**/*,internal/**/*,manifest/**/*,resource/**/*
alwaysApply: false
---
# 后端规则
- 后端使用 GoFrame GF 框架,必须遵循 GF 的项目结构、路由注册、控制器、服务、模型、配置和错误处理规范。
- 分层链路为:路由 -> 控制器 -> service 服务层 -> 模型层。
- 路由层只负责请求路径、HTTP 方法、中间件和控制器绑定,不编写业务逻辑。
- 控制器层负责获取请求信息、参数验证、用户信息解析、调用 service并返回统一响应。
- 控制器层不编写核心业务逻辑,不直接操作数据库,不绕过 service 调用模型。
- service 层是业务逻辑层,负责业务流程编排、权限判断、事务控制、数据处理和外部服务调用。
- 模型层只用于定义表结构、表关系、字段映射和数据承载,不编写业务逻辑。
- 数据库访问、事务、缓存、队列、第三方接口等能力应按 GF 推荐方式封装,并放在合适的服务或基础设施模块中。
- 参数校验、错误码、日志、上下文传递和用户身份解析要保持统一规范。

1
.gitignore vendored
View File

@@ -22,3 +22,4 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
/.cursor/skills/

33
AGENTS.md Normal file
View File

@@ -0,0 +1,33 @@
# 项目协作规则
## 全局规则
- 代码、包、方法、结构体、接口、关键业务分支、复杂判断、关键配置均需要编写清晰的中文注释。
- 注释要说明业务意图、输入输出、边界条件和注意事项,避免只重复代码字面含义。
- 模块职责要清晰,避免跨层调用、重复实现和无关逻辑混入。
- 新增或修改功能时,优先遵循项目已有目录结构、命名方式、封装习惯和错误处理方式。
- 公共能力应沉淀为可复用模块或组件,避免在多个页面、控制器或服务中复制粘贴。
## 前端规则
- 项目名称为“萧康云医官网”,定位为面向诊所赋能业务的 SaaS 平台。
- 产品端形态包含小程序患者端、医生端、门店老板端、推广员端,以及 PC 端管理或官网相关页面。
- 页面设计要体现 SaaS 平台的专业、清晰、高效,优先服务信息扫描、业务转化和多角色理解。
- 必须注意多端适配,兼容移动端、平板端和桌面端,不允许出现文字溢出、布局重叠、关键按钮不可触达等问题。
- 必须提供暗色模式适配,颜色、阴影、边框、图标、图表和状态提示都要在明暗主题下保持可读。
- 采用模块化、组件化设计,页面由清晰的业务模块和可复用 UI 组件组成。
- 组件应保持职责单一,状态、事件、属性和插槽设计清楚,避免单个组件承担过多页面逻辑。
- 表单、列表、筛选、弹窗、空状态、加载态、错误态、成功态等常见状态要完整设计。
- 视觉风格要克制、现代、可信,适合医疗和诊所经营场景;避免过度营销化、装饰化或单一色系堆叠。
## 后端规则
- 后端使用 GoFrame GF 框架,必须遵循 GF 的项目结构、路由注册、控制器、服务、模型、配置和错误处理规范。
- 分层链路为:路由 -> 控制器 -> service 服务层 -> 模型层。
- 路由层只负责请求路径、HTTP 方法、中间件和控制器绑定,不编写业务逻辑。
- 控制器层负责获取请求信息、参数验证、用户信息解析、调用 service并返回统一响应。
- 控制器层不编写核心业务逻辑,不直接操作数据库,不绕过 service 调用模型。
- service 层是业务逻辑层,负责业务流程编排、权限判断、事务控制、数据处理和外部服务调用。
- 模型层只用于定义表结构、表关系、字段映射和数据承载,不编写业务逻辑。
- 数据库访问、事务、缓存、队列、第三方接口等能力应按 GF 推荐方式封装,并放在合适的服务或基础设施模块中。
- 参数校验、错误码、日志、上下文传递和用户身份解析要保持统一规范。

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- 在public/index.html的head中添加 --> <!-- 在public/index.html的head中添加 -->
<!-- <script src="https://webapi.amap.com/maps?v=2.0&key=您的高德地图API密钥"></script>--> <!-- <script src="https://webapi.amap.com/maps?v=2.0&key=您的高德地图API密钥"></script>-->
<title>Vite + Vue</title> <title>萧康云医官网</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

5
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,5 @@
allowBuilds:
'@parcel/watcher': true
core-js: true
esbuild: true
vue-demi: true

16
src/api/request/admin.js Normal file
View File

@@ -0,0 +1,16 @@
import { get, post } from '@/api/request'
// 后台登录,返回 token 与用户基础信息。
export const adminLogin = (data) => post('/admin/login', data)
// 获取后台菜单树,驱动 /admin 分组导航。
export const getAdminMenus = () => get('/admin/menus/tree')
// 获取管理员菜单布局偏好。
export const getMenuLayout = () => get('/admin/profile/menu-layout')
// 更新管理员菜单布局偏好,请求字段遵循后端 snake_case 规则。
export const updateMenuLayout = (menuLayout) => post('/admin/profile/menu-layout', { menu_layout: menuLayout })
// 获取后台角色列表。
export const getRoles = () => get('/admin/roles')

View File

@@ -0,0 +1,7 @@
import { get, post } from '@/api/request'
// 创建套餐订购订单。
export const createOrder = (data) => post('/orders', data)
// 获取后台订单列表。
export const getAdminOrders = () => get('/admin/orders')

View File

@@ -0,0 +1,7 @@
import { get } from '@/api/request'
// 获取官网定价套餐。
export const getPricingPlans = () => get('/pricing/plans')
// 获取后台套餐列表。
export const getAdminPlans = () => get('/admin/pricing/plans')

46
src/api/request/site.js Normal file
View File

@@ -0,0 +1,46 @@
import { get, post } from '@/api/request'
// 获取官网首页聚合内容。
export const getSiteHome = () => get('/site/home')
// 获取前台公开站点配置。
export const getSiteSettings = () => get('/site/settings')
// 获取前台招聘职位。
export const getRecruitJobs = () => get('/recruit/jobs')
// 提交官网咨询线索。
export const createLead = (data) => post('/leads', data)
// 获取后台线索列表。
export const getAdminLeads = () => get('/admin/leads')
// 获取后台页面配置。
export const getAdminSitePages = () => get('/admin/site/pages')
// 保存后台页面配置。
export const saveAdminSitePage = (data) => post('/admin/site/pages', data)
// 获取后台页面模块。
export const getAdminSiteSections = (params = {}) => get('/admin/site/sections', params)
// 保存后台页面模块。
export const saveAdminSiteSection = (data) => post('/admin/site/sections', data)
// 获取后台素材列表。
export const getAdminSiteMedia = (params = {}) => get('/admin/site/media', params)
// 保存后台素材。
export const saveAdminSiteMedia = (data) => post('/admin/site/media', data)
// 获取后台招聘职位。
export const getAdminRecruitJobs = () => get('/admin/recruit/jobs')
// 保存后台招聘职位。
export const saveAdminRecruitJob = (data) => post('/admin/recruit/jobs', data)
// 获取后台站点全局配置。
export const getAdminSiteSettings = () => get('/admin/site/settings')
// 保存后台站点全局配置。
export const saveAdminSiteSettings = (data) => post('/admin/site/settings', data)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
<template>
<div class="admin-shell admin-shell-v2" :class="[`is-${layoutMode}`, { 'is-sidebar-open': sidebarOpen }]">
<AdminSidebar
:menus="menus"
:layout-mode="layoutMode"
:loading="loadingMenus"
@close-mobile="sidebarOpen = false"
/>
<main class="admin-main">
<AdminTopbar
:title="route.meta.title || '后台管理'"
:layout-mode="layoutMode"
:menu-count="menuCount"
@toggle-layout="toggleLayout"
@toggle-sidebar="sidebarOpen = !sidebarOpen"
/>
<section class="admin-page-surface">
<router-view />
</section>
</main>
<button v-if="sidebarOpen" class="admin-mobile-mask" type="button" @click="sidebarOpen = false" aria-label="关闭菜单"></button>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import AdminSidebar from './AdminSidebar.vue'
import AdminTopbar from './AdminTopbar.vue'
import { getAdminMenus, getMenuLayout, updateMenuLayout } from '@/api/request/admin'
const route = useRoute()
const menus = ref([])
const layoutMode = ref('double')
const sidebarOpen = ref(false)
const loadingMenus = ref(true)
// 初始化后台菜单和布局偏好,接口失败时使用最小菜单兜底,保证后台可进入。
onMounted(async () => {
loadingMenus.value = true
menus.value = await getAdminMenus().catch(() => [{ id: 1, title: '控制台', path: '/dashboard', icon: 'dashboard' }])
const layout = await getMenuLayout().catch(() => ({ final_layout: 'double' }))
layoutMode.value = layout.final_layout || 'double'
loadingMenus.value = false
})
const menuCount = computed(() => menus.value.reduce((total, item) => total + 1 + (item.children?.length || 0), 0))
const toggleLayout = async () => {
layoutMode.value = layoutMode.value === 'double' ? 'single' : 'double'
await updateMenuLayout(layoutMode.value).catch(() => null)
}
</script>

View File

@@ -0,0 +1,77 @@
<template>
<aside class="admin-sidebar admin-sidebar-v2" :class="layoutMode">
<div class="admin-sidebar__head">
<div class="admin-sidebar__logo"></div>
<div class="admin-sidebar__brand-copy">
<strong>萧康云医</strong>
<span>Clinic SaaS Console</span>
</div>
<button class="admin-sidebar__close" type="button" @click="$emit('close-mobile')">
<i class="fas fa-times"></i>
</button>
</div>
<div class="admin-sidebar__workspace">
<span>当前空间</span>
<strong>官网运营后台</strong>
</div>
<nav class="admin-sidebar__nav" aria-label="后台菜单">
<div v-if="loading" class="admin-menu-skeleton">
<i></i><i></i><i></i><i></i>
</div>
<template v-else>
<template v-for="item in menus" :key="item.id">
<router-link v-if="!item.children?.length" :to="adminPath(item.path)" class="admin-nav-item-v2" @click="$emit('close-mobile')">
<i :class="iconClass(item.icon)"></i>
<span>{{ item.title }}</span>
</router-link>
<section v-else class="admin-nav-group-v2">
<div class="admin-nav-title-v2">
<i :class="iconClass(item.icon)"></i>
<span>{{ item.title }}</span>
</div>
<router-link v-for="child in item.children" :key="child.id" :to="adminPath(child.path)" class="admin-nav-child-v2" @click="$emit('close-mobile')">
<i :class="iconClass(child.icon)"></i>
<span>{{ child.title }}</span>
<small>{{ child.permission }}</small>
</router-link>
</section>
</template>
</template>
</nav>
</aside>
</template>
<script setup>
defineProps({
menus: { type: Array, default: () => [] },
layoutMode: { type: String, default: 'double' },
loading: { type: Boolean, default: false }
})
defineEmits(['close-mobile'])
const icons = {
dashboard: 'fas fa-chart-pie',
global: 'fas fa-globe-asia',
blocks: 'fas fa-layer-group',
message: 'fas fa-message',
wallet: 'fas fa-wallet',
package: 'fas fa-box-open',
orders: 'fas fa-receipt',
setting: 'fas fa-gear',
user: 'fas fa-user-shield',
safety: 'fas fa-shield-halved',
menu: 'fas fa-bars-staggered',
image: 'fas fa-image',
briefcase: 'fas fa-briefcase'
}
// 后端菜单 path 是后台内部路径,这里统一挂载到 /admin 分组下。
const adminPath = (path) => {
if (!path || path === '/dashboard') return '/admin/dashboard'
return `/admin${path}`
}
const iconClass = (icon) => icons[icon] || 'fas fa-circle-dot'
</script>

View File

@@ -0,0 +1,43 @@
<template>
<header class="admin-topbar admin-topbar-v2">
<button class="admin-icon-btn admin-topbar__menu" type="button" @click="$emit('toggle-sidebar')">
<i class="fas fa-bars"></i>
</button>
<div class="admin-topbar__title">
<div class="admin-breadcrumb">后台管理 / {{ title }}</div>
<h1>{{ title }}</h1>
</div>
<label class="admin-global-search">
<i class="fas fa-search"></i>
<input placeholder="搜索线索、订单、菜单" />
</label>
<div class="admin-topbar__actions">
<button class="admin-chip admin-chip-v2" type="button" @click="$emit('toggle-layout')">
<i class="fas fa-table-columns"></i>
<span>{{ layoutMode === 'double' ? '双列菜单' : '单列菜单' }}</span>
</button>
<button class="admin-icon-btn" type="button" :title="`已加载 ${menuCount} 个菜单节点`">
<i class="fas fa-bell"></i>
</button>
<button class="admin-user-pill" type="button" @click="logout">
<span>AD</span>
<strong>退出</strong>
</button>
</div>
</header>
</template>
<script setup>
defineProps({
title: { type: String, default: '后台管理' },
layoutMode: { type: String, default: 'double' },
menuCount: { type: Number, default: 0 }
})
defineEmits(['toggle-layout', 'toggle-sidebar'])
const logout = () => {
localStorage.removeItem('token')
localStorage.removeItem('user')
window.location.href = '/admin/login'
}
</script>

View File

@@ -0,0 +1,28 @@
<template>
<section class="motion-section journey-section">
<div class="container mx-auto px-4">
<div class="section-intro">
<span class="eyebrow">经营旅程</span>
<h2>首页只讲一件事诊所经营如何从碎片走向连续</h2>
<p>每一段不是一个功能点而是诊所日常经营里真实发生的动作</p>
</div>
<div class="journey-track">
<article v-for="step in steps" :key="step.title" class="journey-card" data-aos="fade-up">
<span>{{ step.index }}</span>
<h3>{{ step.title }}</h3>
<p>{{ step.description }}</p>
</article>
</div>
</div>
</section>
</template>
<script setup>
const steps = [
{ index: '01', title: '被看见', description: '推广码、官网咨询、活动入口先被统一记录,避免线索落在聊天记录里。' },
{ index: '02', title: '被承接', description: '预约和到店意向进入同一个待办池,前台、推广员和医生都知道下一步。' },
{ index: '03', title: '被服务', description: '接诊记录和服务过程沉淀下来,患者不再只是一次到店记录。' },
{ index: '04', title: '被想起', description: '复诊提醒、随访任务和健康建议持续推动关系,而不是结束在离店那一刻。' },
{ index: '05', title: '被看懂', description: '老板看到线索、服务和收入之间的因果,经营动作不再靠猜。' }
]
</script>

View File

@@ -0,0 +1,42 @@
<template>
<section class="motion-section">
<div class="container mx-auto px-4">
<div class="data-current quiet-panel">
<div>
<span class="eyebrow">数据流</span>
<h2>经营数据不是报表是每天都在流动的水位</h2>
<p>从线索到成交从复诊到复购系统持续描绘诊所的经营温度</p>
</div>
<div ref="chartRef" class="data-chart"></div>
</div>
</div>
</section>
</template>
<script setup>
import * as echarts from 'echarts'
import { onBeforeUnmount, onMounted, ref } from 'vue'
const chartRef = ref(null)
let chart
// 初始化轻量动态曲线,用于表达经营流动而不是堆砌仪表盘卡片。
onMounted(() => {
chart = echarts.init(chartRef.value)
chart.setOption({
grid: { left: 20, right: 20, top: 26, bottom: 24 },
xAxis: { type: 'category', show: false, data: ['一', '二', '三', '四', '五', '六', '日'] },
yAxis: { type: 'value', show: false },
series: [
{ type: 'line', smooth: true, symbol: 'none', data: [18, 26, 22, 38, 46, 42, 58], lineStyle: { width: 4, color: '#b48a58' }, areaStyle: { color: 'rgba(180,138,88,.18)' } },
{ type: 'line', smooth: true, symbol: 'none', data: [8, 16, 20, 24, 31, 36, 44], lineStyle: { width: 3, color: '#1f5f50' }, areaStyle: { color: 'rgba(31,95,80,.12)' } }
]
})
window.addEventListener('resize', chart.resize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', chart?.resize)
chart?.dispose()
})
</script>

View File

@@ -0,0 +1,53 @@
<template>
<section class="hero-cinematic" @mousemove="moveGlow">
<img class="hero-cinematic__image" src="@/assets/images/home0001.jpeg" alt="萧康云医诊所赋能场景" />
<div class="hero-cinematic__grain"></div>
<div class="hero-cinematic__glow" :style="glowStyle"></div>
<div class="hero-cinematic__content container mx-auto px-4">
<div class="hero-cinematic__copy">
<div class="art-kicker reveal-up">CLINIC OPERATING OS</div>
<h1 class="reveal-up delay-1">把诊所的获客服务与经营编织成一条会生长的路径</h1>
<p class="reveal-up delay-2">萧康云医不是把功能堆在一起而是让患者医生老板推广员与后台数据在同一个节奏里流动</p>
<div class="hero-cinematic__actions reveal-up delay-3">
<router-link to="/pricing" class="primary-button">预约演示</router-link>
<router-link to="/features" class="secondary-button text-stone-50">探索系统</router-link>
</div>
</div>
<svg class="hero-cinematic__path" viewBox="0 0 760 240" aria-hidden="true">
<path class="path-shadow" d="M20 180 C150 40 230 220 360 100 S560 40 740 150" />
<path class="path-draw" d="M20 180 C150 40 230 220 360 100 S560 40 740 150" />
<g v-for="point in points" :key="point.label" :transform="`translate(${point.x}, ${point.y})`">
<circle r="7" />
<text x="14" y="5">{{ point.label }}</text>
</g>
</svg>
</div>
</section>
</template>
<script setup>
import { computed, ref } from 'vue'
const glow = ref({ x: 68, y: 36 })
const points = [
{ label: '线索', x: 62, y: 146 },
{ label: '预约', x: 205, y: 122 },
{ label: '接诊', x: 360, y: 100 },
{ label: '复诊', x: 530, y: 95 },
{ label: '经营', x: 686, y: 132 }
]
// 根据鼠标位置移动柔光,让首屏有轻微生命感。
const moveGlow = (event) => {
const rect = event.currentTarget.getBoundingClientRect()
glow.value = {
x: Math.round(((event.clientX - rect.left) / rect.width) * 100),
y: Math.round(((event.clientY - rect.top) / rect.height) * 100)
}
}
const glowStyle = computed(() => ({
'--glow-x': `${glow.value.x}%`,
'--glow-y': `${glow.value.y}%`
}))
</script>

View File

@@ -0,0 +1,38 @@
<template>
<section class="motion-section editorial-muted">
<div class="container mx-auto px-4">
<div class="constellation-grid">
<div>
<span class="eyebrow">角色协作</span>
<h2>五个端口不再是五座孤岛</h2>
<p>患者端负责触达医生端负责服务老板端负责判断推广员端负责归因PC 后台负责秩序</p>
</div>
<svg class="constellation" viewBox="0 0 620 420" aria-hidden="true">
<path v-for="line in lines" :key="line" :d="line" class="constellation-line" />
<g v-for="node in nodes" :key="node.title" :transform="`translate(${node.x}, ${node.y})`" class="constellation-node">
<circle r="46" />
<text text-anchor="middle" y="4">{{ node.title }}</text>
</g>
</svg>
</div>
</div>
</section>
</template>
<script setup>
const nodes = [
{ title: '患者端', x: 100, y: 90 },
{ title: '医生端', x: 410, y: 78 },
{ title: '推广员', x: 124, y: 310 },
{ title: '老板端', x: 455, y: 306 },
{ title: 'PC后台', x: 300, y: 205 }
]
const lines = [
'M100 90 C180 110 220 155 300 205',
'M410 78 C365 130 335 160 300 205',
'M124 310 C185 260 235 230 300 205',
'M455 306 C405 270 360 235 300 205',
'M100 90 C205 20 310 18 410 78',
'M124 310 C245 386 352 380 455 306'
]
</script>

View File

@@ -0,0 +1,32 @@
<template>
<section class="motion-section editorial-muted">
<div class="container mx-auto px-4">
<div class="section-intro">
<span class="eyebrow">落地场景</span>
<h2>把复杂系统拆成诊所能立刻理解的三种场景</h2>
</div>
<div class="scenario-grid">
<article v-for="item in scenarios" :key="item.title" class="scenario-card" data-aos="fade-up">
<img :src="item.image" :alt="item.title" />
<div>
<span>{{ item.tag }}</span>
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
</div>
</article>
</div>
</div>
</section>
</template>
<script setup>
import company2 from '@/assets/images/company0002.jpeg'
import company3 from '@/assets/images/company0003.jpeg'
import company5 from '@/assets/images/company0005.jpeg'
const scenarios = [
{ tag: '单店起步', title: '先把预约和线索管起来', description: '把官网咨询、电话到店、推广码都沉淀到同一条跟进线。', image: company2 },
{ tag: '增长运营', title: '让推广和复诊产生回路', description: '推广员知道线索归属,医生知道随访任务,老板知道转化结果。', image: company3 },
{ tag: '连锁预留', title: '为多门店和权限准备秩序', description: '后台菜单、角色和订阅结构提前为门店扩展留好位置。', image: company5 }
]
</script>

View File

@@ -1,101 +1,46 @@
<template> <template>
<footer class="bg-gray-900 text-white pt-16 pb-10 px-4 sm:px-8 lg:px-24"> <footer class="bg-slate-950 px-4 py-12 text-white sm:px-8 lg:px-24">
<div class="container mx-auto"> <div class="container mx-auto">
<div class="flex flex-wrap -mx-4"> <div class="grid gap-10 md:grid-cols-[1.3fr_2fr]">
<!-- 公司信息部分 --> <div>
<div class="w-full md:w-1/3 px-4 mb-8 md:mb-0"> <router-link to="/" class="mb-5 flex items-center">
<div> <img src="@/assets/images/logo-white.png" alt="萧康云医" class="h-10" />
<router-link to="/" class="flex items-center mb-6"> <span class="ml-3 text-2xl font-bold">萧康云医</span>
<img </router-link>
src="@/assets/images/logo-white.png" <p class="max-w-sm text-sm leading-7 text-slate-400">
alt="酉佰" 萧康云医官网面向诊所经营者医生团队和推广团队提供患者端医生端门店老板端推广员端与 PC 后台一体化 SaaS 能力
class="h-8 md:h-10" </p>
/> <div class="mt-6 flex gap-3">
<span class="ml-2 text-xl md:text-2xl font-bold text-white" <a v-for="item in social" :key="item.icon" :href="item.link" class="flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white transition hover:bg-sky-600" aria-label="社交媒体链接">
>酉佰</span <i :class="item.icon"></i>
> </a>
</router-link>
<p class="text-gray-400 mb-4 max-w-xs text-sm sm:text-base">
酉佰专注金融投资领域的专业服务提供金融行业解决方案
</p>
<div class="flex space-x-4">
<a
v-for="(item, index) in social"
:key="index"
:href="item.link"
class="w-8 h-8 sm:w-10 sm:h-10 flex items-center justify-center rounded-full bg-gray-800 hover:bg-primary transition-colors"
aria-label="社交媒体链接"
>
<i :class="item.icon" class="text-white text-sm sm:text-base"></i>
</a>
</div>
</div> </div>
</div> </div>
<!-- 导航链接部分 --> <div class="grid gap-8 sm:grid-cols-3">
<div <div v-for="block in footerLinks" :key="block.title">
v-for="(block, index) in footerLinks" <h3 class="mb-4 text-base font-semibold">{{ block.title }}</h3>
:key="index" <ul class="space-y-3">
class="w-full sm:w-1/2 md:w-1/4 px-4 mb-8 md:mb-0" <li v-for="link in block.links" :key="link.title">
> <router-link v-if="link.path.startsWith('/')" :to="link.path" class="text-sm text-slate-400 transition hover:text-sky-300">
<h3 class="text-base sm:text-lg font-semibold mb-4">{{ block.title }}</h3> {{ link.title }}
<ul class="space-y-2"> </router-link>
<li v-for="(link, linkIndex) in block.links" :key="linkIndex"> <a v-else :href="link.path" class="text-sm text-slate-400 transition hover:text-sky-300">
<a {{ link.title }}
:href="link.path" </a>
class="text-gray-400 hover:text-primary transition-colors text-sm sm:text-base" </li>
> </ul>
{{ link.title }} </div>
</a>
</li>
</ul>
</div> </div>
<!-- 订阅部分 -->
<!-- <div class="w-full md:w-1/4 px-4">-->
<!-- <h3 class="text-base sm:text-lg font-semibold mb-4">订阅我们</h3>-->
<!-- <p class="text-gray-400 mb-4 text-sm sm:text-base">-->
<!-- 订阅我们的邮件获取最新电商行业资讯和解决方案-->
<!-- </p>-->
<!-- <div class="flex">-->
<!-- <input-->
<!-- type="email"-->
<!-- placeholder="您的邮箱地址"-->
<!-- class="w-full px-3 py-2 sm:px-4 sm:py-2 rounded-l-lg focus:outline-none text-gray-700 text-sm sm:text-base"-->
<!-- aria-label="订阅邮箱输入框"-->
<!-- />-->
<!-- <button-->
<!-- class="bg-primary hover:bg-blue-700 text-white font-semibold px-3 sm:px-4 py-2 rounded-r-lg transition-colors text-sm sm:text-base"-->
<!-- aria-label="订阅按钮"-->
<!-- >-->
<!-- 订阅-->
<!-- </button>-->
<!-- </div>-->
<!-- </div>-->
</div> </div>
<!-- 底部版权信息 --> <div class="mt-10 border-t border-white/10 pt-6">
<div class="border-t border-gray-800 mt-10 pt-6"> <div class="flex flex-col items-center justify-between gap-4 text-center text-xs text-slate-500 md:flex-row md:text-left">
<div class="flex flex-col md:flex-row justify-between items-center text-center md:text-left"> <p>© 2026 萧康云医官网. All rights reserved.</p>
<div class="mb-4 md:mb-0"> <div class="flex flex-col items-center gap-3 sm:flex-row">
<p class="text-gray-500 text-xs sm:text-sm"> <a href="https://beian.miit.gov.cn/" class="hover:text-sky-300">沪ICP备2021007173号-1</a>
© 2021 shyoubai.com. All rights reserved <span class="hidden sm:inline">|</span>
</p> <a href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=31010402009014" class="hover:text-sky-300">沪公网安备 31010402009014</a>
</div>
<div class="flex flex-col sm:flex-row justify-center items-center gap-3 text-gray-500 text-sm">
<a href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=31010402009014"
class="flex items-center hover:text-primary transition-colors duration-200 group">
<img src="@/assets/images/gongan.png" alt="公安备案图标" class="h-4 mr-1.5 group-hover:scale-105 transition-transform">
<span>沪公网安备 31010402009014</span>
</a>
<span class="hidden sm:inline-block text-gray-400">|</span>
<a href="https://beian.miit.gov.cn/"
class="flex items-center hover:text-primary transition-colors duration-200 group">
<img src="@/assets/images/icp.png" alt="公安备案图标" class="h-4 mr-1.5 group-hover:scale-105 transition-transform">
<span>沪ICP备2021007173号-1</span>
</a>
</div> </div>
</div> </div>
</div> </div>
@@ -111,30 +56,35 @@ export default {
social: [ social: [
{ icon: 'fab fa-weixin', link: '#' }, { icon: 'fab fa-weixin', link: '#' },
{ icon: 'fab fa-weibo', link: '#' }, { icon: 'fab fa-weibo', link: '#' },
{ icon: 'fab fa-linkedin-in', link: '#' }, { icon: 'fab fa-linkedin-in', link: '#' }
{ icon: 'fab fa-zhihu', link: '#' },
], ],
footerLinks: [ footerLinks: [
{ {
title: '快速导航', title: '产品',
links: [ links: [
{ title: '关于我们', path: '/about' }, { title: '产品能力', path: '/features' },
{ title: '团队介绍', path: '/team' }, { title: '解决方案', path: '/solutions' },
{ title: '服务特色', path: '/features' }, { title: '价格方案', path: '/pricing' }
{ title: '加入我们', path: '/careers' },
] ]
}, },
{ {
title: '联系我们', title: '公司',
links: [
{ title: '关于我们', path: '/about' },
{ title: '团队介绍', path: '/team' },
{ title: '加入我们', path: '/careers' }
]
},
{
title: '联系',
links: [ links: [
{ title: '上海市中山西路2020号华宜大厦2号楼1501室', path: '#' }, { title: '上海市中山西路2020号华宜大厦2号楼1501室', path: '#' },
{ title: '021-34161621', path: 'tel:021-34161621' }, { title: '021-34161621', path: 'tel:021-34161621' },
{ title: 'youbaiwangluo@163.com', path: 'mailto:youbaiwangluo@163.com' }, { title: 'youbaiwangluo@163.com', path: 'mailto:youbaiwangluo@163.com' }
{ title: '周一至周五 9:00-18:00', path: '#' },
] ]
} }
] ]
} }
} }
} }
</script> </script>

View File

@@ -1,304 +1,43 @@
<template> <template>
<header <header class="fixed inset-x-0 top-0 z-50 border-b border-transparent transition-all duration-300" :class="headerClass">
class="fixed top-0 w-full z-50 transition-all duration-300" <div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
:class="{ <router-link to="/" class="flex min-w-0 items-center">
'bg-white/90 backdrop-blur-md shadow-sm py-3': isScrolled || isMenuOpen, <img :src="logoSrc" alt="萧康云医" class="h-9 w-9 shrink-0 rounded-lg object-contain" @error="logoSrc = defaultLogo" />
'bg-transparent py-5': !isScrolled && !isMenuOpen <div class="ml-3 min-w-0">
}" <div class="truncate text-base font-bold text-slate-950 dark-text-strong">{{ brandName }}</div>
> <div class="hidden text-xs text-slate-500 sm:block dark-text-muted">诊所赋能 SaaS 平台</div>
<!-- 关键修复移动端布局优化 -->
<div class="px-3 sm:px-4">
<div class="flex justify-between items-center">
<router-link to="/" class="flex items-center shrink-0">
<img src="@/assets/images/logo.png" alt="Logo" class="w-8 h-8 md:w-10 md:h-10" />
<span class="ml-2 text-base sm:text-xl md:text-2xl font-bold text-primary whitespace-nowrap">
{{ currentLang === 'zh' ? '酉佰' : 'YouBaiBai' }}
</span>
</router-link>
<nav class="hidden md:flex space-x-8">
<router-link
v-for="item in menuItems"
:key="item.path"
:to="item.path"
class="font-medium transition-colors hover:text-primary"
active-class="text-primary"
:exact="item.exact"
>
{{ item.name }}
</router-link>
</nav>
<div class="hidden md:flex items-center space-x-4">
<!-- 语言切换按钮 -->
<button
@click="toggleLanguage"
class="flex items-center justify-center w-10 h-10 rounded-full bg-gray-100 hover:bg-primary hover:text-white transition-colors"
:title="currentLang === 'zh' ? '切换为英文' : 'Switch to Chinese'"
>
{{ currentLang === 'zh' ? 'EN' : '中' }}
</button>
<button
class="bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 text-white font-medium py-2 px-4 md:py-3 md:px-6 rounded-lg flex items-center justify-center transition-all duration-300 transform hover:-translate-y-1 shadow-lg hover:shadow-xl active:translate-y-0 active:shadow-md"
@click="openModal"
>
{{ currentLang === 'zh' ? '联系我们' : 'Contact' }}
</button>
</div> </div>
</router-link>
<!-- 移动端菜单按钮 - 关键修复区域 --> <nav class="hidden items-center gap-1 lg:flex"><template v-for="item in menuItems" :key="item.path"><div v-if="item.children" class="nav-dropdown"><router-link :to="item.path" class="rounded-full px-4 py-2 text-sm font-medium text-slate-700 transition hover:text-[var(--gold)] dark-text" active-class="text-[var(--gold)] dark-active">{{ item.name }}<i class="fas fa-chevron-down ml-1 text-[10px]"></i></router-link><div class="nav-dropdown__panel"><router-link v-for="child in item.children" :key="child.path" :to="child.path" class="nav-dropdown__item"><span>{{ child.name }}</span><small>{{ child.desc }}</small></router-link></div></div><router-link v-else :to="item.path" class="rounded-full px-4 py-2 text-sm font-medium text-slate-700 transition hover:text-[var(--gold)] dark-text" active-class="text-[var(--gold)] dark-active" :exact="item.exact">{{ item.name }}</router-link></template></nav>
<div class="flex items-center space-x-2 md:hidden"> <div class="hidden items-center gap-3 lg:flex"><button class="icon-button" type="button" :title="isDark ? '切换为明亮模式' : '切换为暗色模式'" @click="toggleTheme"><i :class="isDark ? 'fas fa-sun' : 'fas fa-moon'"></i></button><button class="primary-button" type="button" @click="openModal"><i class="fas fa-calendar-check"></i><span>预约演示</span></button></div>
<!-- 移动端语言切换按钮 - 尺寸优化 --> <div class="flex items-center gap-2 lg:hidden"><button class="icon-button" type="button" :title="isDark ? '切换为明亮模式' : '切换为暗色模式'" @click="toggleTheme"><i :class="isDark ? 'fas fa-sun' : 'fas fa-moon'"></i></button><button class="icon-button" type="button" title="打开菜单" @click="toggleMenu"><i :class="isMenuOpen ? 'fas fa-times' : 'fas fa-bars'"></i></button></div>
<button
@click="toggleLanguage"
class="shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 hover:bg-primary hover:text-white transition-colors"
>
{{ currentLang === 'zh' ? 'EN' : '中' }}
</button>
<button @click="toggleMenu" class="shrink-0 text-gray-700">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16m-7 6h7"
/>
</svg>
</button>
</div>
</div>
</div>
<!-- 移动端菜单 - 修复宽度问题 -->
<div
v-show="isMenuOpen"
class="w-full md:hidden fixed inset-x-0 z-50 bg-white/95 backdrop-blur-lg py-5 overflow-hidden"
:style="{ top: headerHeight + 'px', height: `calc(100vh - ${headerHeight}px)` }"
@click.self="closeMenu"
>
<div class="px-4 h-full flex flex-col">
<nav class="flex flex-col space-y-4 py-4 overflow-y-auto">
<router-link
v-for="item in menuItems"
:key="item.path"
:to="item.path"
class="font-medium text-lg py-2 transition-colors hover:text-primary"
active-class="text-primary"
@click="closeMenu"
>
{{ item.name }}
</router-link>
</nav>
<div class="pt-4 pb-8 mt-auto">
<button
class="w-full bg-primary hover:bg-blue-700 text-white font-semibold py-3 rounded-lg transition-colors"
@click="openModal(); closeMenu();"
>
{{ currentLang === 'zh' ? '联系我们' : 'Contact' }}
</button>
</div>
</div>
</div> </div>
<div v-show="isMenuOpen" class="border-t border-slate-100 bg-white/95 px-4 py-4 shadow-xl backdrop-blur lg:hidden dark-panel"><nav class="mx-auto flex max-w-7xl flex-col gap-2"><template v-for="item in menuItems" :key="item.path"><router-link :to="item.path" class="rounded-lg px-3 py-3 text-base font-medium text-slate-700 hover:text-[var(--gold)] dark-text" active-class="text-[var(--gold)] dark-active" @click="closeMenu">{{ item.name }}</router-link><div v-if="item.children" class="mobile-subnav"><router-link v-for="child in item.children" :key="child.path" :to="child.path" @click="closeMenu">{{ child.name }}</router-link></div></template><button class="primary-button mt-2 w-full justify-center" type="button" @click="openModal(); closeMenu();"><i class="fas fa-calendar-check"></i><span>预约演示</span></button></nav></div>
</header> </header>
<transition name="modal-fade"><div v-if="showModal" class="fixed inset-0 z-50 flex items-start justify-center overflow-auto bg-slate-950/50 p-4 backdrop-blur" @click="closeModal"><div class="mt-16 w-full max-w-2xl overflow-hidden rounded-xl bg-white shadow-2xl dark-panel" @click.stop><div class="flex items-center justify-between bg-[var(--pine-deep)] px-5 py-4 text-white"><div><h2 class="text-xl font-bold">预约萧康云医演示</h2><p class="mt-1 text-sm text-stone-200">留下联系方式,我们会根据诊所情况提供演示方案。</p></div><button class="rounded-full p-2 hover:bg-white/10" type="button" @click="closeModal"><i class="fas fa-times"></i></button></div><div class="bg-[var(--pine-deep)] p-5"><ContactForm lang="zh" /></div></div></div></transition>
<!-- 模态框组件 -->
<!-- 模态框组件 - 移动端优化 -->
<transition name="modal-fade">
<div v-if="showModal" @click="closeModal" class="fixed inset-0 z-50 overflow-auto bg-black/50 flex items-start justify-center p-4 backdrop-blur-lg">
<div @click.stop class="bg-white rounded-2xl shadow-xl w-full max-w-md sm:max-w-2xl overflow-hidden transform transition-all sm:mt-20">
<!-- 模态框头部 -->
<div class="bg-gradient-to-r from-indigo-600 to-blue-700 p-4 sm:p-6 text-white">
<div class="flex justify-between items-center">
<h2 class="text-xl sm:text-2xl font-bold">{{ currentLang === 'zh' ? '留下您的联系方式' : 'Leave Your Contact' }}</h2>
<button @click="closeModal" class="text-white hover:text-gray-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- 模态框内容 - 移动端优化 -->
<div class="p-4 sm:p-6 max-h-[70vh] overflow-y-auto bg-gradient-to-r from-indigo-600 to-blue-700">
<ContactForm :lang="currentLang" />
</div>
<!-- 模态框底部 - 移动端优化 -->
<div class="bg-gray-50 px-4 sm:px-6 py-4 flex flex-col sm:flex-row sm:flex-wrap justify-end gap-3">
<button
@click="closeModal"
class="w-full sm:w-auto px-5 py-2 text-gray-700 rounded-lg font-medium border border-gray-300 hover:bg-gray-100"
>
{{ currentLang === 'zh' ? '关闭' : 'Close' }}
</button>
<button
class="w-full sm:w-auto px-5 py-2 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700"
>
{{ currentLang === 'zh' ? '立即申请' : 'Submit' }}
</button>
</div>
</div>
</div>
</transition>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'; import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import ContactForm from "@/components/ui/ContactForm.vue"; import ContactForm from '@/components/ui/ContactForm.vue'
import { getSiteSettings } from '@/api/request/site'
const isScrolled = ref(false); import defaultLogo from '@/assets/images/logo.png'
const isMenuOpen = ref(false); const isScrolled = ref(false), isMenuOpen = ref(false), showModal = ref(false), isDark = ref(false)
const showModal = ref(false); const brandName = ref('萧康云医')
const headerHeight = ref(0); const logoSrc = ref(defaultLogo)
const currentLang = ref('zh'); // 默认中文 const menuItems = [{ path: '/', name: '首页', exact: true }, { path: '/features', name: '产品能力' }, { path: '/solutions', name: '解决方案' }, { path: '/pricing', name: '价格' }, { path: '/about', name: '关于我们', children: [{ path: '/about', name: '公司介绍', desc: '品牌使命与业务定位' }, { path: '/team', name: '团队介绍', desc: '医疗与技术团队' }, { path: '/features', name: '服务特色', desc: '诊所赋能服务体系' }, { path: '/careers', name: '加入我们', desc: '招聘与共创机会' }] }]
const headerClass = computed(() => isScrolled.value || isMenuOpen.value ? 'bg-white/90 shadow-sm backdrop-blur dark-panel' : 'bg-white/75 backdrop-blur dark-panel')
// 新增:视口和导航宽度变量 const applyTheme = () => { document.documentElement.classList.toggle('dark', isDark.value); localStorage.setItem('xk-theme', isDark.value ? 'dark' : 'light') }
const viewportWidth = ref(0); const toggleTheme = () => { isDark.value = !isDark.value; applyTheme() }
const navWidth = ref(0); const openModal = () => { showModal.value = true }
const closeModal = () => { showModal.value = false }
// 菜单项根据语言动态变化 const toggleMenu = () => { isMenuOpen.value = !isMenuOpen.value; document.body.style.overflow = isMenuOpen.value ? 'hidden' : '' }
const menuItems = computed(() => { const closeMenu = () => { isMenuOpen.value = false; document.body.style.overflow = '' }
return [ const handleScroll = () => { isScrolled.value = window.scrollY > 10 }
{ path: '/', name: currentLang.value === 'zh' ? '首页' : 'Home', exact: true }, onMounted(async () => { const savedTheme = localStorage.getItem('xk-theme'); isDark.value = savedTheme ? savedTheme === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches; applyTheme(); window.addEventListener('scroll', handleScroll); const settings = await getSiteSettings().catch(() => null); if (settings?.site_title) brandName.value = settings.site_title.replace('官网', ''); if (settings?.site_logo && !settings.site_logo.startsWith('/src/')) logoSrc.value = settings.site_logo })
{ path: '/about', name: currentLang.value === 'zh' ? '公司介绍' : 'About' }, onBeforeUnmount(() => { window.removeEventListener('scroll', handleScroll); document.body.style.overflow = '' })
{ path: '/team', name: currentLang.value === 'zh' ? '团队介绍' : 'Team' },
{ path: '/features', name: currentLang.value === 'zh' ? '服务特色' : 'Features' },
{ path: '/careers', name: currentLang.value === 'zh' ? '加入我们' : 'Careers' }
];
});
// 新增:输出视口和导航宽度
const logDimensions = () => {
setTimeout(function () {
// 获取视口宽度
viewportWidth.value = window.innerWidth;
// 获取导航元素宽度
const navElement = document.querySelector('header');
if (navElement) {
navWidth.value = navElement.offsetWidth;
}
console.log(`触发: ${viewportWidth.value}`);
// 修改header标签的宽度
navElement.style.width = `${navWidth.value}px`;
// 修改body的宽度
document.body.style.width = `${viewportWidth.value}px`
}, 800);
};
const toggleLanguage = () => {
currentLang.value = currentLang.value === 'zh' ? 'en' : 'zh';
};
const openModal = () => {
showModal.value = true;
};
const closeModal = () => {
showModal.value = false;
};
const toggleMenu = () => {
isMenuOpen.value = !isMenuOpen.value;
if (isMenuOpen.value) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
};
const closeMenu = () => {
isMenuOpen.value = false;
document.body.style.overflow = '';
};
const handleScroll = () => {
isScrolled.value = window.scrollY > 20;
};
// 计算头部高度用于移动菜单定位
const calculateHeaderHeight = () => {
const header = document.querySelector('header');
if (header) {
headerHeight.value = header.offsetHeight;
}
};
onMounted(() => {
window.addEventListener('scroll', handleScroll);
calculateHeaderHeight();
window.addEventListener('resize', calculateHeaderHeight);
// 新增:添加尺寸输出功能
logDimensions(); // 初始输出
window.addEventListener('resize', logDimensions); // 窗口大小变化时输出
});
onBeforeUnmount(() => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', calculateHeaderHeight);
// 新增:移除尺寸输出监听器
window.removeEventListener('resize', logDimensions);
document.body.style.overflow = ''; // 确保卸载时恢复滚动
});
</script> </script>
<style scoped> <style scoped>
.router-link-exact-active { .modal-fade-enter-active,.modal-fade-leave-active { transition: opacity 0.2s ease; }.modal-fade-enter-from,.modal-fade-leave-to { opacity: 0; }.nav-dropdown { position: relative; }.nav-dropdown__panel { position: absolute; left: 50%; top: calc(100% + 12px); display: grid; width: 224px; transform: translateX(-50%) translateY(8px); border: 1px solid var(--line); border-radius: 10px; background: color-mix(in srgb, var(--panel-bg) 96%, transparent); padding: 8px; opacity: 0; pointer-events: none; box-shadow: 0 24px 60px rgba(0, 0, 0, 0.14); transition: opacity 0.2s ease, transform 0.2s ease; }.nav-dropdown:hover .nav-dropdown__panel { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0); }.nav-dropdown__item { display: grid; gap: 2px; border-radius: 8px; padding: 10px 12px; color: var(--ink); }.nav-dropdown__item:hover { background: var(--page-muted); color: var(--gold); }.nav-dropdown__item small { color: var(--text-muted); font-size: 12px; }.mobile-subnav { display: grid; gap: 4px; margin: -4px 0 8px 18px; border-left: 1px solid var(--line); padding-left: 12px; }.mobile-subnav a { border-radius: 8px; padding: 8px 10px; color: var(--text-main); font-size: 14px; }
position: relative; </style>
}
.router-link-exact-active::after {
content: '';
position: absolute;
bottom: -3px;
left: 0;
width: 100%;
height: 2px;
background-color: #2563eb;
}
/* 模态框动画 */
.modal-fade-enter-active,
.modal-fade-leave-active {
transition: opacity 0.3s ease;
}
.modal-fade-enter-from,
.modal-fade-leave-to {
opacity: 0;
}
/* 移动端菜单宽度修复 */
@media (max-width: 767px) {
.container {
padding-left: 1rem;
padding-right: 1rem;
}
/* 确保移动菜单不会超出视口 */
.overflow-hidden {
overflow: hidden;
}
/* 防止路由链接换行 */
.router-link {
white-space: nowrap;
}
}
/* 增强模糊效果 */
.backdrop-blur-lg {
backdrop-filter: blur(12px);
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<article class="editorial-card" :class="tone" :data-aos="aos">
<span v-if="index" class="editorial-card__index">{{ index }}</span>
<slot name="media"></slot>
<div class="editorial-card__body">
<p v-if="eyebrow" class="editorial-card__eyebrow">{{ eyebrow }}</p>
<h3>{{ title }}</h3>
<p>{{ description }}</p>
<slot></slot>
</div>
</article>
</template>
<script setup>
defineProps({
title: { type: String, required: true },
description: { type: String, required: true },
eyebrow: { type: String, default: '' },
index: { type: String, default: '' },
tone: { type: String, default: 'plain' },
aos: { type: String, default: 'fade-up' }
})
</script>

View File

@@ -0,0 +1,22 @@
<template>
<article class="feature-line-card" :data-aos="aos">
<span>{{ index }}</span>
<div>
<h3>{{ title }}</h3>
<p>{{ description }}</p>
<div v-if="items?.length" class="feature-line-card__tags">
<em v-for="item in items" :key="item">{{ item }}</em>
</div>
</div>
</article>
</template>
<script setup>
defineProps({
index: { type: String, required: true },
title: { type: String, required: true },
description: { type: String, required: true },
items: { type: Array, default: () => [] },
aos: { type: String, default: 'fade-up' }
})
</script>

View File

@@ -1,84 +1,82 @@
<!-- src/components/MapContainer.vue --> <template>
<template> <div class="map-shell">
<div id="map-container" class="w-full h-full"></div> <div v-if="canLoadMap" ref="mapRef" class="map-canvas" :aria-label="title"></div>
<div v-if="!canLoadMap || hasError" class="map-fallback">
<div>
<span>公司位置</span>
<h3>{{ title }}</h3>
<p>{{ address }}</p>
<small>经纬度{{ lng }}, {{ lat }}</small>
</div>
<a :href="mapLink" target="_blank" rel="noreferrer">打开高德地图</a>
</div>
<div v-if="loading && canLoadMap" class="map-loading">地图加载中...</div>
</div>
</template> </template>
<script setup> <script setup>
import { onMounted, defineProps } from 'vue' import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import AMapLoader from '@amap/amap-jsapi-loader' import AMapLoader from '@amap/amap-jsapi-loader'
const props = defineProps({ const props = defineProps({
lng: { lng: { type: Number, required: true },
type: Number, lat: { type: Number, required: true },
required: true zoom: { type: Number, default: 16 },
}, title: { type: String, default: '公司位置' },
lat: { address: { type: String, default: '' },
type: Number, amapKey: { type: String, default: import.meta.env.VITE_AMAP_KEY || '' },
required: true securityJsCode: { type: String, default: import.meta.env.VITE_AMAP_SECURITY_CODE || '' }
},
zoom: {
type: Number,
default: 16
},
title: {
type: String,
default: '公司位置'
},
address: {
type: String,
default: ''
}
}) })
onMounted(() => { const mapRef = ref(null)
// 安全密钥配置 const mapInstance = ref(null)
window._AMapSecurityConfig = { const loading = ref(false)
securityJsCode: "test123" const hasError = ref(false)
} const canLoadMap = computed(() => Boolean(props.amapKey && props.securityJsCode))
const mapLink = computed(() => `https://uri.amap.com/marker?position=${props.lng},${props.lat}&name=${encodeURIComponent(props.title)}&src=xiaokang&coordinate=gaode&callnative=1`)
AMapLoader.load({ const initMap = async () => {
key: "testkey", if (!canLoadMap.value) return
version: "2.0", loading.value = true
plugins: ["AMap.InfoWindow"] hasError.value = false
}).then((AMap) => { try {
// 初始化地图 window._AMapSecurityConfig = { securityJsCode: props.securityJsCode }
const map = new AMap.Map('map-container', { const AMap = await AMapLoader.load({ key: props.amapKey, version: '2.0', plugins: ['AMap.InfoWindow'] })
zoom: props.zoom, await nextTick()
center: [props.lng, props.lat], if (!mapRef.value) return
viewMode: '3D' mapInstance.value = new AMap.Map(mapRef.value, { zoom: props.zoom, center: [props.lng, props.lat], viewMode: '3D' })
}) const marker = new AMap.Marker({ position: [props.lng, props.lat], title: props.title })
mapInstance.value.add(marker)
// 添加标记
const marker = new AMap.Marker({
position: [props.lng, props.lat],
title: props.title
})
map.add(marker)
// 添加信息窗口
const infoWindow = new AMap.InfoWindow({ const infoWindow = new AMap.InfoWindow({
content: `<div class="p-2"> content: `<div style="padding:8px 10px"><strong>${props.title}</strong><p style="margin:6px 0 0;font-size:12px;line-height:1.6">${props.address}</p></div>`,
<h4 class="font-bold">${props.title}</h4>
<p class="text-sm">${props.address}</p>
</div>`,
offset: new AMap.Pixel(0, -30) offset: new AMap.Pixel(0, -30)
}) })
infoWindow.open(mapInstance.value, [props.lng, props.lat])
marker.on('click', () => infoWindow.open(mapInstance.value, [props.lng, props.lat]))
} catch (error) {
console.error('地图加载失败', error)
hasError.value = true
} finally {
loading.value = false
}
}
// 默认打开信息窗口 onMounted(initMap)
infoWindow.open(map, [props.lng, props.lat]) onBeforeUnmount(() => {
mapInstance.value?.destroy?.()
// 点击标记打开信息窗口 mapInstance.value = null
marker.on('click', () => {
infoWindow.open(map, [props.lng, props.lat])
})
}).catch(e => {
console.error('地图加载失败', e)
})
}) })
</script> </script>
<style scoped> <style scoped>
#map-container { .map-shell { position: relative; min-height: 380px; overflow: hidden; border: 1px solid var(--line); border-radius: 14px; background: var(--panel-bg); }
min-height: 300px; .map-canvas { width: 100%; min-height: 380px; }
} .map-loading { position: absolute; inset: 0; display: grid; place-items: center; background: color-mix(in srgb, var(--panel-bg) 70%, transparent); color: var(--text-main); }
.map-fallback { min-height: 380px; display: flex; align-items: end; justify-content: space-between; gap: 24px; padding: 28px; background: linear-gradient(135deg, color-mix(in srgb, var(--panel-bg) 88%, #fff 12%), var(--page-muted)); }
.map-fallback span { color: var(--gold); font-size: 13px; font-weight: 800; }
.map-fallback h3 { margin-top: 10px; color: var(--ink); font-size: clamp(28px, 4vw, 48px); font-weight: 900; }
.map-fallback p { margin-top: 10px; color: var(--text-main); line-height: 1.8; }
.map-fallback small { display: block; margin-top: 10px; color: var(--text-muted); }
.map-fallback a { flex: 0 0 auto; border: 1px solid var(--line); border-radius: 999px; padding: 12px 18px; color: var(--ink); font-weight: 800; }
@media (max-width: 720px) { .map-fallback { align-items: stretch; flex-direction: column; } .map-fallback a { text-align: center; } }
</style> </style>

View File

@@ -0,0 +1,16 @@
<template>
<article class="metric-art-card" :data-aos="aos">
<span>{{ label }}</span>
<strong>{{ value }}</strong>
<p>{{ description }}</p>
</article>
</template>
<script setup>
defineProps({
label: { type: String, required: true },
value: { type: String, required: true },
description: { type: String, required: true },
aos: { type: String, default: 'zoom-in' }
})
</script>

View File

@@ -0,0 +1,33 @@
<template>
<article class="pricing-row-card" :class="recommended ? 'is-recommended' : ''" data-aos="fade-up">
<div>
<div class="pricing-row-card__title">
<h3>{{ name }}</h3>
<span v-if="recommended">推荐</span>
</div>
<p>{{ description }}</p>
</div>
<div class="pricing-row-card__price">
<strong>{{ price }}</strong>
<small>/{{ unit }}</small>
</div>
<ul>
<li v-for="item in features" :key="item.name">
<span>{{ item.name }}</span>{{ item.value }}
</li>
</ul>
<button class="secondary-button justify-center whitespace-nowrap" type="button" @click="$emit('select')">选择{{ name }}</button>
</article>
</template>
<script setup>
defineProps({
name: { type: String, required: true },
description: { type: String, required: true },
price: { type: String, required: true },
unit: { type: String, required: true },
features: { type: Array, default: () => [] },
recommended: { type: Boolean, default: false }
})
defineEmits(['select'])
</script>

View File

@@ -0,0 +1,23 @@
<template>
<article class="scenario-image-card" :class="reverse ? 'is-reverse' : ''">
<div class="scenario-image-card__media" data-aos="fade-right">
<img :src="image" :alt="title" />
</div>
<div class="scenario-image-card__copy" data-aos="fade-left">
<span>{{ label }}</span>
<h3>{{ title }}</h3>
<p>{{ description }}</p>
<slot></slot>
</div>
</article>
</template>
<script setup>
defineProps({
label: { type: String, required: true },
title: { type: String, required: true },
description: { type: String, required: true },
image: { type: String, required: true },
reverse: { type: Boolean, default: false }
})
</script>

View File

@@ -1,13 +1,36 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user' import { getSiteSettings } from '@/api/request/site'
let cachedSiteSettings = null
// 在路由配置中添加个人中心路由
const routes = [ const routes = [
{ path: '/', meta: { title: '首页' }, name: 'Home', component: () => import('@/views/Home.vue') }, { path: '/', meta: { title: '首页', page_code: 'home' }, name: 'Home', component: () => import('@/views/Home.vue') },
{ path: '/about', meta: { title: '公司介绍' }, name: 'About', component: () => import('@/views/About.vue') }, { path: '/features', meta: { title: '产品能力', page_code: 'features' }, name: 'Features', component: () => import('@/views/Features.vue') },
{ path: '/team', meta: { title: '团队介绍' }, name: 'Team', component: () => import('@/views/Team.vue') }, { path: '/solutions', meta: { title: '解决方案', page_code: 'solutions' }, name: 'Solutions', component: () => import('@/views/Solutions.vue') },
{ path: '/features', meta: { title: '公司特色' }, name: 'Features', component: () => import('@/views/Features.vue') }, { path: '/pricing', meta: { title: '价格方案', page_code: 'pricing' }, name: 'Pricing', component: () => import('@/views/Pricing.vue') },
{ path: '/careers', meta: { title: '加入我们' }, name: 'Careers', component: () => import('@/views/Careers.vue') }, { path: '/about', meta: { title: '关于我们', page_code: 'about' }, name: 'About', component: () => import('@/views/About.vue') },
{ path: '/team', meta: { title: '团队介绍', page_code: 'team' }, name: 'Team', component: () => import('@/views/Team.vue') },
{ path: '/careers', meta: { title: '加入我们', page_code: 'careers' }, name: 'Careers', component: () => import('@/views/Careers.vue') },
{ path: '/admin/login', meta: { title: '后台登录', adminPublic: true }, name: 'AdminLogin', component: () => import('@/views/admin/AdminLogin.vue') },
{
path: '/admin',
component: () => import('@/components/admin/AdminLayout.vue'),
redirect: '/admin/dashboard',
meta: { requiresAdmin: true },
children: [
{ path: 'dashboard', meta: { title: '控制台', requiresAdmin: true }, component: () => import('@/views/admin/AdminDashboard.vue') },
{ path: 'site/leads', meta: { title: '咨询线索', requiresAdmin: true }, component: () => import('@/views/admin/AdminLeads.vue') },
{ path: 'site/sections', meta: { title: '页面模块', requiresAdmin: true }, component: () => import('@/views/admin/AdminSections.vue') },
{ path: 'site/media', meta: { title: '图片管理', requiresAdmin: true }, component: () => import('@/views/admin/AdminMedia.vue') },
{ path: 'site/jobs', meta: { title: '招聘管理', requiresAdmin: true }, component: () => import('@/views/admin/AdminJobs.vue') },
{ path: 'site/settings', meta: { title: '站点设置', requiresAdmin: true }, component: () => import('@/views/admin/AdminSiteSettings.vue') },
{ path: 'commerce/plans', meta: { title: '套餐管理', requiresAdmin: true }, component: () => import('@/views/admin/AdminPlans.vue') },
{ path: 'commerce/orders', meta: { title: '订单管理', requiresAdmin: true }, component: () => import('@/views/admin/AdminOrders.vue') },
{ path: 'system/roles', meta: { title: '角色权限', requiresAdmin: true }, component: () => import('@/views/admin/AdminRoles.vue') },
{ path: 'system/menus', meta: { title: '菜单管理', requiresAdmin: true }, component: () => import('@/views/admin/AdminMenus.vue') },
{ path: 'settings/profile', meta: { title: '个人设置', requiresAdmin: true }, component: () => import('@/views/admin/AdminProfile.vue') }
]
}
] ]
const router = createRouter({ const router = createRouter({
@@ -18,19 +41,45 @@ const router = createRouter({
} }
}) })
const applyFavicon = (href) => {
if (!href) return
let link = document.querySelector('link[rel="icon"]')
if (!link) {
link = document.createElement('link')
link.rel = 'icon'
document.head.appendChild(link)
}
link.href = href
}
const applyMeta = (name, content) => {
if (!content) return
let meta = document.querySelector(`meta[name="${name}"]`)
if (!meta) {
meta = document.createElement('meta')
meta.setAttribute('name', name)
document.head.appendChild(meta)
}
meta.setAttribute('content', content)
}
const ensureSiteSettings = async () => {
if (cachedSiteSettings) return cachedSiteSettings
cachedSiteSettings = await getSiteSettings().catch(() => ({ site_title: '萧康云医官网' }))
return cachedSiteSettings
}
router.beforeEach(async (to) => {
if (to.meta.requiresAdmin && !localStorage.getItem('token')) {
return { name: 'AdminLogin', query: { redirect: to.fullPath } }
}
const settings = await ensureSiteSettings()
const siteTitle = settings.site_title || '萧康云医官网'
document.title = to.meta.title ? `${to.meta.title} | ${siteTitle}` : siteTitle
applyFavicon(settings.site_favicon)
applyMeta('description', settings.default_seo?.description || '萧康云医为诊所提供全链路经营赋能。')
applyMeta('keywords', settings.default_seo?.keywords || '诊所SaaS,云医,诊所管理')
applyMeta('x-gso-summary', settings.default_gso?.summary || '诊所赋能 SaaS 平台')
})
export default router export default router
router.beforeEach((to, from) => {
// const userStore = useUserStore()
// if (to.meta.requiresAuth && !userStore.user) {
// return { name: 'Login', query: { redirect: to.fullPath } }
// }
if (to.meta.title) {
console.log('to.meta.title', to.meta)
document.title = `${to.meta.title} | 上海酉佰`
} else {
document.title = `上海酉佰`
}
})

View File

@@ -21,12 +21,12 @@
<div class="order-2 lg:order-1" data-aos="fade-right" data-aos-duration="800"> <div class="order-2 lg:order-1" data-aos="fade-right" data-aos-duration="800">
<SectionTitle <SectionTitle
subtitle="关于我们" subtitle="关于我们"
title="我们是上海酉佰" title="我们是萧康云医"
/> />
<div class="prose max-w-none text-gray-600 mt-6"> <div class="prose max-w-none text-gray-600 mt-6">
<p> <p>
上海酉佰有限公司成立于2010年总部位于上海浦东新区 萧康云医专注诊所赋能与医疗服务数字化总部位于上海
是一家专注于提供企业级数字化解决方案的高新技术企业 是一家专注于提供企业级数字化解决方案的高新技术企业
</p> </p>
<p> <p>
@@ -176,7 +176,7 @@
title="发展历程" title="发展历程"
subtitle="一路成长" subtitle="一路成长"
align="center" align="center"
description="回顾上海酉佰发展中的重要里程碑" description="回顾萧康云医发展中的重要里程碑"
data-aos="fade-up" data-aos="fade-up"
/> />

View File

@@ -1,719 +1,177 @@
<template> <template>
<div> <div class="page-shell careers-page-v2">
<!-- 页面标题 --> <section class="subpage-hero careers-hero">
<div
class="pt-32 pb-20 bg-gradient-to-r from-indigo-700 to-blue-900"
data-aos="fade-down"
>
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="text-center"> <div class="subpage-hero__grid">
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">加入我们</h1> <div data-aos="fade-up">
<p class="text-xl text-blue-300 max-w-2xl mx-auto"> <span class="subpage-kicker">JOIN XIAOKANG</span>
与优秀团队一起成长创造改变世界的价值 <h1>和我们一起把诊所经营变得更清楚</h1>
</p> <p>萧康云医正在寻找理解医疗服务也愿意打磨产品细节的伙伴这里的工作不只是做页面或做运营而是把真实诊所的复杂日常整理成可靠系统</p>
</div>
<div class="careers-hero__panel" data-aos="fade-left">
<strong>{{ filteredJobs.length }}</strong>
<span>开放职位</span>
<p>技术产品运营和诊所服务顾问持续招聘中</p>
</div>
</div> </div>
</div> </div>
</div> </section>
<!-- 职位列表 --> <section class="editorial-section">
<section class="py-16 md:py-24">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div class="careers-grid">
<div class="lg:col-span-1"> <aside class="careers-filter" data-aos="fade-right">
<SectionTitle <span class="eyebrow">职位筛选</span>
title="开放职位" <div class="careers-search">
subtitle="寻找卓越人才" <i class="fas fa-search"></i>
data-aos="fade-right" <input v-model="keyword" placeholder="搜索职位、部门或城市" />
/>
<div
class="bg-gray-50 rounded-xl p-6 mt-10"
data-aos="fade-right"
data-aos-delay="100"
>
<h3 class="font-medium text-gray-900 mb-4">职位筛选</h3>
<div class="space-y-3">
<div class="flex items-center">
<input id="filter-all" type="radio" name="job-type" class="h-4 w-4 text-indigo-600" checked>
<label for="filter-all" class="ml-2 text-gray-700">所有职位</label>
</div>
<div class="flex items-center">
<input id="filter-tech" type="radio" name="job-type" class="h-4 w-4 text-indigo-600">
<label for="filter-tech" class="ml-2 text-gray-700">技术类</label>
</div>
<div class="flex items-center">
<input id="filter-product" type="radio" name="job-type" class="h-4 w-4 text-indigo-600">
<label for="filter-product" class="ml-2 text-gray-700">产品类</label>
</div>
<div class="flex items-center">
<input id="filter-design" type="radio" name="job-type" class="h-4 w-4 text-indigo-600">
<label for="filter-design" class="ml-2 text-gray-700">设计类</label>
</div>
<div class="flex items-center">
<input id="filter-business" type="radio" name="job-type" class="h-4 w-4 text-indigo-600">
<label for="filter-business" class="ml-2 text-gray-700">市场类</label>
</div>
</div>
</div> </div>
<div class="careers-tabs">
<div <button
class="bg-amber-50 rounded-xl p-6 mt-8" v-for="item in departments"
data-aos="fade-right" :key="item"
data-aos-delay="200" :class="{ active: department === item }"
> type="button"
<h3 class="font-medium text-gray-900 mb-4">加入我们的优势</h3> @click="department = item"
<ul class="space-y-3">
<li v-for="(benefit, index) in benefits" :key="index" class="flex items-start">
<i class="fas fa-check-circle text-amber-600 mt-1 mr-2"></i>
<span class="text-gray-700">{{ benefit }}</span>
</li>
</ul>
</div>
<!-- 联系方式卡片 -->
<div
class="bg-blue-50 rounded-xl p-6 mt-8"
data-aos="fade-right"
data-aos-delay="300"
>
<h3 class="font-medium text-gray-900 mb-4">联系我们</h3>
<div class="space-y-3 text-gray-700">
<div class="flex items-start">
<i class="fas fa-phone-alt text-blue-600 mt-1 mr-3"></i>
<span>021-34161621</span>
</div>
<div class="flex items-start">
<i class="fas fa-envelope text-blue-600 mt-1 mr-3"></i>
<span>youbaiwangluo@163.com</span>
</div>
<div class="flex items-start">
<i class="fas fa-map-marker-alt text-blue-600 mt-1 mr-3"></i>
<span>上海市中山西路2020号华宜大厦2号楼1501室</span>
</div>
</div>
</div>
</div>
<div class="lg:col-span-2">
<div
class="flex items-center justify-between mb-8"
data-aos="fade-left"
>
<h2 class="text-2xl font-bold text-gray-900">
<span class="text-amber-600">{{ jobs.length }}</span> 个开放职位
</h2>
<div class="relative w-64">
<input
type="text"
placeholder="搜索职位..."
class="w-full pl-10 pr-4 py-2 bg-gray-50 rounded-lg border border-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-200 focus:border-transparent"
>
<i class="fas fa-search absolute left-3 top-2.5 text-gray-400"></i>
</div>
</div>
<div class="space-y-6">
<div
v-for="(job, index) in jobs"
:key="index"
class="bg-white rounded-xl shadow-sm border border-gray-200 hover:border-indigo-200 hover:shadow-md transition-all duration-300"
:data-aos="index % 2 === 0 ? 'fade-right' : 'fade-left'"
:data-aos-delay="100 * index"
> >
<div class="p-6"> {{ item }}
<div class="flex flex-col md:flex-row justify-between md:items-center">
<div>
<h3 class="text-xl font-bold text-gray-900">{{ job.title }}</h3>
<div class="mt-1 flex flex-wrap gap-2">
<span class="text-sm bg-blue-50 text-blue-700 px-3 py-1 rounded-full">{{ job.department }}</span>
<span class="text-sm bg-amber-50 text-amber-700 px-3 py-1 rounded-full">{{ job.type }}</span>
<span class="text-sm bg-gray-50 text-gray-700 px-3 py-1 rounded-full">{{ job.location }}</span>
</div>
</div>
<button
class="mt-4 md:mt-0 px-5 py-2.5 text-white rounded-lg font-medium transition-colors duration-300"
:class="job.featured ? 'bg-amber-600 hover:bg-amber-700' : 'bg-indigo-600 hover:bg-indigo-700'"
>
立即申请
</button>
</div>
<div class="mt-6 flex items-center text-sm text-gray-500">
<span>发布日期: {{ job.date }}</span>
<span class="mx-2"></span>
<span>{{ job.salary }}</span>
</div>
</div>
<div class="border-t border-gray-100 px-6 py-4 flex justify-between items-center">
<div class="flex items-center">
<div class="w-8 h-8 rounded-md bg-indigo-50 flex items-center justify-center text-indigo-600">
<i class="fas fa-briefcase"></i>
</div>
<span class="ml-2 text-sm text-gray-600">{{ job.experience }}经验</span>
</div>
<a
href="#"
class="text-indigo-600 hover:text-indigo-800 flex items-center text-sm"
@click.prevent="openModal(job)"
>
查看详情
<i class="fas fa-arrow-right ml-1 text-xs"></i>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 招聘流程 -->
<section class="py-16 md:py-24 bg-gray-50">
<div class="container mx-auto px-4">
<SectionTitle
title="招聘流程"
subtitle="透明高效"
description="我们简化招聘流程,让候选人拥有清晰明确的体验"
align="center"
data-aos="fade-up"
/>
<div class="mt-16">
<div class="flex justify-center">
<div class="relative">
<!-- 流程线 -->
<div class="absolute top-14 left-0 right-0 h-1 bg-gray-200 mx-auto z-0 w-4/5" style="left: 10%;"></div>
<div class="grid grid-cols-1 md:grid-cols-5 gap-8 relative z-10">
<div
v-for="(step, index) in recruitmentSteps"
:key="index"
class="text-center"
data-aos="fade-up"
:data-aos-delay="index * 100"
>
<div class="w-20 h-20 rounded-full flex items-center justify-center mx-auto text-xl bg-white border-4 border-gray-50 shadow-md mb-4"
:class="index === 0 ? 'text-blue-500 border-blue-100' :
index === 1 ? 'text-amber-500 border-amber-100' :
index === 2 ? 'text-green-500 border-green-100' :
index === 3 ? 'text-purple-500 border-purple-100' :
'text-indigo-500 border-indigo-100'">
<i :class="step.icon"></i>
</div>
<h3 class="font-bold text-lg text-gray-900">{{ step.title }}</h3>
<p class="text-gray-600 text-sm mt-2">{{ step.description }}</p>
<div class="text-xs mt-2 font-medium text-gray-500">
{{ step.duration }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 员工福利 -->
<section class="py-16 md:py-24">
<div class="container mx-auto px-4">
<SectionTitle
title="员工福利"
subtitle="关怀每一位伙伴"
description="我们为员工提供全方位的福利保障和贴心关怀"
align="center"
data-aos="fade-up"
/>
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 mt-16">
<div
v-for="(benefit, index) in employeeBenefits"
:key="index"
data-aos="fade-up"
:data-aos-delay="index * 50"
>
<Card no-border hover-effect class="h-full">
<template #icon>
<div class="w-16 h-16 rounded-lg mx-auto mb-6 flex items-center justify-center text-2xl"
:class="benefit.iconBgClass">
<i :class="benefit.icon"></i>
</div>
</template>
<h3 class="text-lg font-bold text-center text-gray-900">{{ benefit.title }}</h3>
<p class="text-center text-gray-600 mt-2">
{{ benefit.description }}
</p>
</Card>
</div>
</div>
</div>
</section>
<!-- 校招计划 -->
<section
class="py-16 md:py-24 bg-gradient-to-r from-indigo-900 to-blue-900 text-white"
data-aos="fade-up"
>
<div class="container mx-auto px-4">
<div class="max-w-4xl mx-auto">
<div class="flex flex-col md:flex-row items-center">
<div
class="md:w-2/5 mb-10 md:mb-0"
data-aos="fade-right"
>
<div class="bg-gradient-to-br from-blue-500 to-indigo-600 p-10 rounded-2xl flex items-center justify-center">
<i class="fas fa-user-graduate text-6xl text-white opacity-40"></i>
</div>
</div>
<div
class="md:w-3/5 md:pl-12"
data-aos="fade-left"
>
<SectionTitle
title="校园招聘计划"
subtitle="新生力量"
align="left"
class="text-white"
/>
<p class="text-xl text-blue-200 mb-6">
我们为应届毕业生和在校学生提供专属的职业发展计划
</p>
<ul class="space-y-4 mb-8">
<li
v-for="(item, index) in campusPlan"
:key="index"
class="flex items-start"
data-aos="fade-left"
:data-aos-delay="index * 50"
>
<div class="w-6 h-6 rounded-full bg-blue-500 flex items-center justify-center text-white text-xs mr-3">
{{ index+1 }}
</div>
<span class="flex-1">{{ item }}</span>
</li>
</ul>
<router-link
to="/campus"
class="inline-flex items-center px-6 py-3 bg-white text-indigo-900 rounded-lg font-medium hover:bg-gray-100 transition-colors"
data-aos="fade-up"
>
了解校招计划
<i class="fas fa-arrow-right ml-2"></i>
</router-link>
</div>
</div>
</div>
</div>
</section>
<!-- 在模态框的地图部分替换为 -->
<section
class="py-16 md:py-24 bg-gray-50"
data-aos="fade-up"
>
<div class="mt-8 container mx-auto px-4">
<h3 class="text-lg font-bold text-gray-900 mb-3">公司位置</h3>
<MapContainer
:lng="121.424"
:lat="31.197"
:title="'上海酉佰网络信息科技有限公司'"
:address="'上海市中山西路2020号华宜大厦2号楼1501室'"
/>
</div>
</section>
<!-- 模态框组件 -->
<transition name="modal-fade">
<div v-if="showModal" @click="closeModal" class="fixed inset-0 z-50 overflow-auto bg-black/50 bg-opacity-50 flex items-center justify-center p-4 backdrop-blur-lg ">
<div @click.stop class="bg-white rounded-2xl shadow-xl w-full max-w-3xl overflow-hidden transform transition-all">
<!-- 模态框头部 -->
<div class="bg-gradient-to-r from-indigo-600 to-blue-700 p-6 text-white">
<div class="flex justify-between items-center">
<h2 class="text-2xl font-bold">{{ currentJob.title }}</h2>
<button @click="closeModal" class="text-white hover:text-gray-200">
<i class="fas fa-times text-xl"></i>
</button> </button>
</div> </div>
<div class="mt-2 flex flex-wrap gap-2"> <div class="careers-contact-card">
<span class="text-sm bg-blue-500 text-white px-3 py-1 rounded-full">{{ currentJob.department }}</span> <h3>投递方式</h3>
<span class="text-sm bg-amber-500 text-white px-3 py-1 rounded-full">{{ currentJob.type }}</span> <p>请将简历发送至招聘邮箱邮件标题注明职位名称</p>
<span class="text-sm bg-gray-700 text-white px-3 py-1 rounded-full">{{ currentJob.location }}</span> <a :href="'mailto:' + contactEmail">{{ contactEmail }}</a>
</div> </div>
</div> </aside>
<!-- 模态框内容 --> <div class="careers-list">
<div class="p-6 max-h-[70vh] overflow-y-auto"> <article v-for="job in filteredJobs" :key="job.id" class="career-job-card" data-aos="fade-up">
<!-- 职位详情 --> <div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <span>{{ job.department }} / {{ job.city }}</span>
<div class="md:col-span-2"> <h2>{{ job.title }}</h2>
<h3 class="text-lg font-bold text-gray-900 mb-3">职位描述</h3> <p>{{ job.description }}</p>
<p class="text-gray-700 mb-4">{{ currentJob.description }}</p> <div class="career-job-card__tags">
<em>{{ job.job_type }}</em>
<h3 class="text-lg font-bold text-gray-900 mb-3">岗位要求</h3> <em>{{ job.salary }}</em>
<ul class="list-disc pl-5 space-y-2 text-gray-700"> <em>{{ job.experience }}</em>
<li v-for="(requirement, idx) in currentJob.requirements" :key="idx">{{ requirement }}</li> <em v-if="job.featured">重点招聘</em>
</ul>
</div>
<div class="border-l border-gray-200 pl-6">
<div class="mb-6">
<h3 class="text-lg font-bold text-gray-900 mb-3">负责人</h3>
<div class="flex items-center">
<div class="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center text-gray-500">
<i class="fas fa-user"></i>
</div>
<div class="ml-3">
<p class="font-medium">{{ currentJob.manager }}</p>
<p class="text-sm text-gray-600">{{ currentJob.managerTitle }}</p>
</div>
</div>
</div>
<div class="mb-6">
<h3 class="text-lg font-bold text-gray-900 mb-3">联系方式</h3>
<div class="space-y-2">
<div class="flex items-center">
<i class="fas fa-envelope text-indigo-600 mr-2"></i>
<a :href="'mailto:' + currentJob.email" class="text-indigo-600 hover:underline">{{ currentJob.email }}</a>
</div>
<div class="flex items-center">
<i class="fas fa-phone-alt text-indigo-600 mr-2"></i>
<span>{{ currentJob.phone }}</span>
</div>
</div>
</div>
<div>
<h3 class="text-lg font-bold text-gray-900 mb-3">照片要求</h3>
<div class="bg-gray-50 rounded-lg p-4">
<p class="text-sm text-gray-700">{{ currentJob.photoRequirement }}</p>
</div>
</div> </div>
</div> </div>
</div> <button class="secondary-button" type="button" @click="openJob(job)">查看详情</button>
</article>
<!-- &lt;!&ndash; 地图容器 &ndash;&gt;--> <div v-if="!filteredJobs.length" class="careers-empty">暂无匹配职位请换个关键词试试</div>
<!-- <div class="mt-8">-->
<!-- <h3 class="text-lg font-bold text-gray-900 mb-3">公司位置</h3>-->
<!-- <div id="job-map-container" class="h-64 rounded-xl overflow-hidden border border-gray-200"></div>-->
<!-- </div>-->
</div>
<!-- 模态框底部 -->
<div class="bg-gray-50 px-6 py-4 flex justify-end">
<button
@click="closeModal"
class="px-5 py-2 text-gray-700 rounded-lg font-medium border border-gray-300 mr-3 hover:bg-gray-100"
>
关闭
</button>
<button
class="px-5 py-2 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700"
>
立即申请
</button>
</div> </div>
</div> </div>
</div> </div>
</section>
<section class="editorial-section editorial-muted">
<div class="container mx-auto px-4">
<div class="career-process">
<div data-aos="fade-right">
<span class="eyebrow">招聘流程</span>
<h2>流程透明反馈清楚</h2>
</div>
<div class="career-process__steps">
<div v-for="(step, index) in steps" :key="step.title" data-aos="fade-up" :data-aos-delay="index * 80">
<span>0{{ index + 1 }}</span>
<h3>{{ step.title }}</h3>
<p>{{ step.desc }}</p>
</div>
</div>
</div>
</div>
</section>
<section class="editorial-section">
<div class="container mx-auto px-4">
<div class="section-intro" data-aos="fade-up">
<span class="eyebrow">公司位置</span>
<h2>欢迎来聊聊真实诊所和真实产品</h2>
</div>
<MapContainer
:lng="116.3969921072894"
:lat="39.91848141642785"
title="萧康医药"
address="杭州市萧山区鸿盛路与高新六路交叉口东200米"
:amap-key="siteSettings.amap_key || ''"
:security-js-code="siteSettings.amap_security_code || ''"
/>
</div>
</section>
<transition name="modal-fade">
<div v-if="currentJob" class="career-modal" @click="currentJob = null">
<article @click.stop>
<button class="career-modal__close" type="button" @click="currentJob = null" aria-label="关闭职位详情"><i class="fas fa-times"></i></button>
<span>{{ currentJob.department }} / {{ currentJob.city }}</span>
<h2>{{ currentJob.title }}</h2>
<p>{{ currentJob.description }}</p>
<h3>岗位要求</h3>
<ul>
<li v-for="item in currentJob.requirements" :key="item">{{ item }}</li>
</ul>
<div class="career-modal__meta">
<div><strong>负责人</strong><p>{{ currentJob.manager }} · {{ currentJob.manager_title }}</p></div>
<div><strong>联系方式</strong><p>{{ currentJob.email }} / {{ currentJob.phone }}</p></div>
</div>
</article>
</div>
</transition> </transition>
</div> </div>
</template> </template>
<script setup> <script setup>
import SectionTitle from '@/components/ui/SectionTitle.vue' import { computed, onMounted, ref } from 'vue'
import Card from '@/components/ui/Card.vue' import MapContainer from '@/components/ui/MapContainer.vue'
import {nextTick, onMounted, ref} from 'vue' import { getRecruitJobs, getSiteSettings } from '@/api/request/site'
import AOS from 'aos'
import 'aos/dist/aos.css'
import MapContainer from "@/components/ui/MapContainer.vue";
// // 初始化AOS const keyword = ref('')
// onMounted(() => { const department = ref('全部')
// AOS.init({ const jobs = ref([])
// duration: 800, const siteSettings = ref({})
// offset: 100, const currentJob = ref(null)
// easing: 'ease-in-out',
// once: true
// })
//
// })
const position = { const fallbackJobs = [
lng: 121.424,
lat: 31.197,
title: '上海酉佰网络信息科技有限公司',
address: '上海市中山西路2020号华宜大厦2号楼1501室'
}
const benefits = [
'富有竞争力的薪资待遇',
'全面的福利保障体系',
'灵活的工作时间和地点',
'专业的职业发展支持',
'多元包容的企业文化',
'丰厚的绩效奖金和股票期权'
]
const recruitmentSteps = [
{
icon: 'fas fa-file-alt',
title: '提交申请',
description: '在线提交您的简历',
duration: '1-3个工作日'
},
{
icon: 'fas fa-phone-alt',
title: '简历评估',
description: 'HR电话初步沟通',
duration: '2-4个工作日'
},
{
icon: 'fas fa-laptop-code',
title: '技术面试',
description: '专业能力技术评估',
duration: '1-2次面试'
},
{
icon: 'fas fa-user-friends',
title: '经理面试',
description: '团队文化和价值观评估',
duration: '1次面试'
},
{
icon: 'fas fa-award',
title: '录用通知',
description: '发放正式录用offer',
duration: '1-2个工作日'
}
]
const employeeBenefits = [
{
title: '薪酬福利',
icon: 'fas fa-money-bill-wave',
description: '有竞争力的薪资及绩效奖金',
iconBgClass: 'bg-green-100 text-green-600'
},
{
title: '五险一金',
icon: 'fas fa-shield-alt',
description: '全面的社会保障和住房公积金',
iconBgClass: 'bg-blue-100 text-blue-600'
},
{
title: '带薪休假',
icon: 'fas fa-umbrella-beach',
description: '灵活的年假、病假及各类假期',
iconBgClass: 'bg-amber-100 text-amber-600'
},
{
title: '健康关怀',
icon: 'fas fa-heartbeat',
description: '年度体检及补充商业保险',
iconBgClass: 'bg-red-100 text-red-600'
},
{
title: '学习发展',
icon: 'fas fa-graduation-cap',
description: '专业培训与个人发展基金',
iconBgClass: 'bg-purple-100 text-purple-600'
},
{
title: '弹性工作',
icon: 'fas fa-laptop-house',
description: '灵活工作时间及远程办公选项',
iconBgClass: 'bg-indigo-100 text-indigo-600'
},
{
title: '团队活动',
icon: 'fas fa-users',
description: '丰富多彩的团队建设活动',
iconBgClass: 'bg-pink-100 text-pink-600'
},
{
title: '节日福利',
icon: 'fas fa-gift',
description: '节日礼品及员工关怀礼包',
iconBgClass: 'bg-yellow-100 text-yellow-600'
}
]
const campusPlan = [
'面向应届毕业生的专项培养计划',
'技术大牛导师一对一指导',
'轮岗实践了解公司业务',
'快速成长通道和晋升机会',
'应届生专属落户支持',
'人才公寓或住房补贴'
]
// 模态框控制
const showModal = ref(false)
const currentJob = ref({})
// 打开模态框
const openModal = (job) => {
currentJob.value = job
showModal.value = true
// 初始化地图
nextTick(() => {
// initMap()
})
}
// 关闭模态框
const closeModal = () => {
showModal.value = false
// 销毁地图实例
if (window.mapInstance) {
window.mapInstance.destroy()
window.mapInstance = null
}
}
// 初始化地图
function initMap() {
// 确保地图容器存在
const container = document.getElementById('job-map-container')
if (!container) return
// 检查是否已加载高德地图API
if (!window.AMap) {
console.error('高德地图API未加载')
return
}
// 销毁旧地图实例
if (window.mapInstance) {
window.mapInstance.destroy()
}
// 创建新地图实例
window.mapInstance = new AMap.Map('job-map-container', {
zoom: 16,
center: [121.424, 31.197], // 公司位置坐标
viewMode: '3D'
})
// 添加标记
new AMap.Marker({
position: [121.424, 31.197],
map: window.mapInstance,
title: '上海酉佰网络信息科技有限公司'
})
// 添加信息窗口
const infoWindow = new AMap.InfoWindow({
content: `<div class="p-2">
<h4 class="font-bold">上海酉佰</h4>
<p class="text-sm">上海市中山西路2020号华宜大厦2号楼1501室</p>
</div>`,
offset: new AMap.Pixel(0, -30)
})
infoWindow.open(window.mapInstance, [121.424, 31.197])
}
// 更新职位数据,添加详细信息
const jobs = ref([
{ {
id: 1,
title: '高级前端开发工程师', title: '高级前端开发工程师',
department: '技术部', department: '技术部',
type: '全职', job_type: '全职',
location: '上海', city: '杭州',
date: '2023-07-10',
salary: '25K-45K', salary: '25K-45K',
experience: '5年以上', experience: '5年以上',
featured: true, featured: true,
// 新增字段 description: '负责官网、后台和小程序前端体验,建立稳定的组件体系。',
description: '负责公司核心产品的前端架构设计与开发,优化用户体验和前端性能,与产品、设计团队紧密合作实现产品需求。', requirements: ['熟悉 Vue 3 与工程化', '有中后台或 SaaS 项目经验', '关注性能和可访问性'],
requirements: [
'精通Vue.js框架及其生态系统',
'熟悉前端工程化、模块化开发',
'有大型项目架构设计经验',
'良好的团队协作和沟通能力',
'计算机相关专业本科以上学历'
],
manager: '张经理', manager: '张经理',
managerTitle: '技术总监', manager_title: '技术负责人',
email: 'tech@shyoubai.com', email: 'tech@xiaokang.example',
phone: '021-34161621', phone: '0571-00000000'
photoRequirement: '请提供近期免冠证件照白底和生活照各一张尺寸为2寸文件大小不超过5MB。' }
}, ]
{ const steps = [
title: '行业研究员', { title: '简历投递', desc: '通过邮箱或后台职位入口提交简历。' },
department: '研究部', { title: '初步沟通', desc: '确认经历、意向和岗位匹配度。' },
type: '全职', { title: '专业面试', desc: '围绕真实业务问题展开交流。' },
location: '上海', { title: '录用沟通', desc: '明确薪酬、职责和入职安排。' }
date: '2023-07-15', ]
salary: '20K-35K',
experience: '3年以上',
featured: true,
// 新增字段
description: '负责行业研究分析,定期撰写研究报告,建立研究框架和数据库,进行模拟交易和投资策略研究。',
requirements: [
'硕士及以上学历,经济、金融、理工类专业',
'具备证券/基金/期货从业资格优先',
'熟悉资本市场和投资分析方法',
'优秀的逻辑思维和分析能力',
'良好的数据敏感度和研究能力'
],
manager: '王总监',
managerTitle: '研究部负责人',
email: 'research@shyoubai.com',
phone: '021-34161622',
photoRequirement: '请提供近期免冠证件照蓝底和全身职业照各一张尺寸为2寸文件大小不超过5MB。'
},
// 其他职位类似添加...
])
</script>
<style scoped> const contactEmail = computed(() => siteSettings.value.contact_email || 'hr@xiaokang.example')
/* 校招计划中的SectionTitle需要调整文字颜色 */ const departments = computed(() => ['全部', ...new Set(jobs.value.map(job => job.department).filter(Boolean))])
.text-white ::v-deep .section-title { const filteredJobs = computed(() => jobs.value.filter((job) => {
color: white; const text = keyword.value.trim().toLowerCase()
} const matchText = !text || [job.title, job.department, job.city, job.description].some(value => String(value || '').toLowerCase().includes(text))
const matchDepartment = department.value === '全部' || job.department === department.value
return matchText && matchDepartment
}))
.text-white ::v-deep .section-subtitle { const openJob = (job) => { currentJob.value = job }
color: #93c5fd;
}
.text-white ::v-deep .section-divider { onMounted(async () => {
background: rgba(255,255,255,0.3); const [jobsResult, settingsResult] = await Promise.allSettled([getRecruitJobs(), getSiteSettings()])
} if (settingsResult.status === 'fulfilled') siteSettings.value = settingsResult.value || {}
jobs.value = jobsResult.status === 'fulfilled' ? jobsResult.value : fallbackJobs
/* 地图容器样式 */ if (!jobs.value.length) jobs.value = fallbackJobs
#map-container { })
min-height: 400px; </script>
background-color: #f0f4f8;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="%23e5e7eb"/><path d="M0,0 L100,100 M100,0 L0,100" stroke="%23d1d5db" stroke-width="1"/></svg>');
background-size: 20px 20px;
display: flex;
align-items: center;
justify-content: center;
color: #6b7280;
font-weight: 500;
}
/* 模态框动画 */
.modal-fade-enter-active,
.modal-fade-leave-active {
transition: opacity 0.3s ease;
}
.modal-fade-enter-from,
.modal-fade-leave-to {
opacity: 0;
}
/* 地图容器样式 */
#job-map-container {
min-height: 256px;
}
</style>

View File

@@ -1,514 +1,71 @@
<template> <template>
<div> <div class="page-shell refined-page features-refined">
<!-- 页面标题 - 使用公司文化背景图 --> <section class="subpage-hero">
<div
class="relative pt-32 pb-20 bg-gradient-to-r from-blue-800 to-indigo-900"
data-aos="fade-down"
data-aos-duration="800"
>
<!-- 背景图片 -->
<div class="absolute inset-0 z-0">
<img
src="../assets/images/features-home.png"
alt="公司文化背景图"
class="w-full h-full object-cover"
/>
</div>
<div class="container mx-auto px-4 relative z-10">
<div class="text-center">
<h1
class="text-4xl md:text-5xl font-bold text-white mb-4"
data-aos="fade-up"
data-aos-delay="200"
>
公司文化
</h1>
<p
class="text-xl text-blue-100 max-w-2xl mx-auto"
data-aos="fade-up"
data-aos-delay="300"
>
以学习驱动成长以渐进实现卓越
</p>
</div>
</div>
</div>
<!-- 学习文化部分 -->
<section class="py-16 md:py-24">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<SectionTitle <div class="subpage-hero__grid">
title="学习文化" <div data-aos="fade-up">
subtitle="研究与投资的持久战" <span class="subpage-kicker">PRODUCT CAPABILITY</span>
description="保持学习,不断思考是持久战的基础" <h1>把诊所的每个角色放进同一套工作秩序</h1>
align="center" <p>产品能力页专注说明模块边界患者端医生端老板端推广员端和 PC 后台如何协作而不是重复首页叙事</p>
data-aos="fade-up"
/>
<div class="grid md:grid-cols-2 gap-12 mt-16">
<div class="flex flex-col justify-center">
<div class="space-y-6">
<div
class="flex items-start"
data-aos="fade-right"
data-aos-delay="100"
>
<div class="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center text-blue-600 mr-4 flex-shrink-0">
<i class="fas fa-book-open text-xl"></i>
</div>
<div>
<h3 class="text-xl font-semibold text-gray-800 mb-2">日常学习机制</h3>
<p class="text-gray-600">
我们建立每日晨读每周分享每月专题研讨的学习体系营造持续学习氛围让知识更新成为团队习惯
</p>
</div>
</div>
<div
class="flex items-start"
data-aos="fade-right"
data-aos-delay="200"
>
<div class="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center text-blue-600 mr-4 flex-shrink-0">
<i class="fas fa-user-graduate text-xl"></i>
</div>
<div>
<h3 class="text-xl font-semibold text-gray-800 mb-2">深度入职培训</h3>
<p class="text-gray-600">
新员工将接受为期2个月的系统培训涵盖行业研究方法宏观分析框架企业评估模型等专业内容建立扎实的知识基础
</p>
</div>
</div>
<div
class="flex items-start"
data-aos="fade-right"
data-aos-delay="300"
>
<div class="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center text-blue-600 mr-4 flex-shrink-0">
<i class="fas fa-lightbulb text-xl"></i>
</div>
<div>
<h3 class="text-xl font-semibold text-gray-800 mb-2">知识共享平台</h3>
<p class="text-gray-600">
建立内部知识库汇集行业研究报告项目复盘总结方法论工具包等资源实现知识资产的有效沉淀与共享
</p>
</div>
</div>
</div>
</div> </div>
<div class="capability-map" data-aos="fade-left">
<div <div v-for="node in capabilityNodes" :key="node.title" :class="node.class">
class="relative rounded-xl overflow-hidden shadow-lg" <span>{{ node.label }}</span>
data-aos="fade-left" <strong>{{ node.title }}</strong>
data-aos-delay="400"
>
<div class="h-full bg-gradient-to-br from-blue-50 to-indigo-50 p-8 flex items-center">
<div class="text-center w-full">
<div class="mb-8 flex justify-center">
<div class="w-20 h-20 rounded-full bg-blue-100 flex items-center justify-center text-blue-600">
<i class="fas fa-brain text-3xl"></i>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-800 mb-4">学习型组织</h3>
<p class="text-gray-600 max-w-md mx-auto">
我们相信持续学习是保持竞争力的核心通过系统化的知识管理和培训体系打造深度思考能力为长期发展蓄能
</p>
<div class="mt-8">
<div class="inline-flex items-center px-4 py-2 bg-white border border-blue-200 text-blue-600 rounded-full text-sm font-medium">
<i class="fas fa-book mr-2"></i>
查看学习资源
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- 团队理念部分大道不孤众行致远 --> <section class="editorial-section">
<section class="py-16 md:py-24">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<SectionTitle <div class="feature-split">
title="团队理念" <aside data-aos="fade-right">
subtitle="大道不孤,众行致远" <span class="eyebrow">能力地图</span>
description="在酉佰,我们坚信真正的成功来自团队协作与共同成长" <h2>先定义角色再定义数据如何流动</h2>
align="center" <p>诊所经营的混乱往往来自角色之间的断点萧康云医用端口拆分工作用后台统一沉淀</p>
data-aos="fade-up" </aside>
/> <div class="role-matrix">
<article v-for="role in roles" :key="role.title" data-aos="fade-up">
<div <span>{{ role.index }}</span>
class="bg-white rounded-2xl shadow-xl overflow-hidden mt-16" <h3>{{ role.title }}</h3>
data-aos="fade-up" <p>{{ role.description }}</p>
data-aos-delay="200" <ul><li v-for="item in role.items" :key="item">{{ item }}</li></ul>
> </article>
<div class="grid md:grid-cols-2">
<!-- 左侧理念详情 -->
<div class="p-8 md:p-12 bg-gradient-to-br from-blue-50 to-indigo-50">
<div class="flex items-center mb-8">
<div class="w-16 h-16 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 mr-4">
<i class="fas fa-hands-helping text-2xl"></i>
</div>
<h2 class="text-3xl font-bold text-gray-800">大道不孤众行致远</h2>
</div>
<p class="text-gray-600 text-lg mb-8">
在酉佰我们坚信真正的成功来自团队协作与共同成长个人的力量有限但团队的力量无穷
</p>
<div class="space-y-6">
<div
class="flex items-start"
data-aos="fade-right"
data-aos-delay="300"
>
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 mr-4 flex-shrink-0">
<i class="fas fa-users"></i>
</div>
<div>
<h3 class="text-xl font-semibold text-gray-800 mb-1">协作共赢</h3>
<p class="text-gray-600">
打破部门壁垒建立跨职能协作机制共享知识与资源实现1+1>2的协同效应
</p>
</div>
</div>
<div
class="flex items-start"
data-aos="fade-right"
data-aos-delay="400"
>
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 mr-4 flex-shrink-0">
<i class="fas fa-hands"></i>
</div>
<div>
<h3 class="text-xl font-semibold text-gray-800 mb-1">互助成长</h3>
<p class="text-gray-600">
建立导师制度与伙伴计划经验丰富的成员带领新人成长形成互帮互助的学习网络
</p>
</div>
</div>
<div
class="flex items-start"
data-aos="fade-right"
data-aos-delay="500"
>
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 mr-4 flex-shrink-0">
<i class="fas fa-road"></i>
</div>
<div>
<h3 class="text-xl font-semibold text-gray-800 mb-1">长远发展</h3>
<p class="text-gray-600">
着眼长期价值而非短期利益与客户伙伴建立持久合作关系共同创造可持续的未来
</p>
</div>
</div>
</div>
</div>
<!-- 右侧视觉展示 -->
<div class="p-8 md:p-12 bg-gradient-to-br from-indigo-600 to-blue-800 flex items-center justify-center">
<div class="text-center max-w-md">
<div class="mb-8">
<i class="fas fa-people-carry text-5xl text-white opacity-90"></i>
</div>
<h3 class="text-2xl font-bold text-white mb-4">团队协作的力量</h3>
<p class="text-blue-100 mb-8">
共同的目标共享的价值观协作的智慧让我们在复杂多变的环境中稳步前行
</p>
<div class="relative h-40 mx-auto">
<svg viewBox="0 0 500 200" class="w-full h-40">
<!-- 团队协作路径 -->
<path
d="M50,150 C150,50 200,100 250,50 C300,100 350,50 450,150"
stroke="#c7d2fe"
stroke-width="4"
fill="none"
stroke-dasharray="1000"
class="path-animation"
/>
<!-- 团队成员 -->
<g transform="translate(50,150)">
<circle cx="0" cy="0" r="8" fill="#818cf8" />
<text x="12" y="5" fill="#c7d2fe" font-size="12">技术</text>
</g>
<g transform="translate(150,50)">
<circle cx="0" cy="0" r="8" fill="#60a5fa" />
<text x="12" y="5" fill="#c7d2fe" font-size="12">研究</text>
</g>
<g transform="translate(250,50)">
<circle cx="0" cy="0" r="8" fill="#38bdf8" />
<text x="12" y="5" fill="#c7d2fe" font-size="12">咨询</text>
</g>
<g transform="translate(350,50)">
<circle cx="0" cy="0" r="8" fill="#22d3ee" />
<text x="12" y="5" fill="#c7d2fe" font-size="12">客户</text>
</g>
<g transform="translate(450,150)">
<circle cx="0" cy="0" r="8" fill="#2dd4bf" />
<text x="12" y="5" fill="#c7d2fe" font-size="12">成功</text>
</g>
</svg>
</div>
<p class="text-blue-100 text-sm mt-6">
技术研究咨询与客户共同协作共创成功之路
</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- 循序渐进部分 --> <section class="editorial-section editorial-muted">
<section class="py-16 md:py-24 bg-gradient-to-r from-amber-50 to-amber-100">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<SectionTitle <div class="process-slice">
title="循序渐进" <div data-aos="fade-up">
subtitle="顺其自然,脚踏实地" <span class="eyebrow">流程切片</span>
description="不盲目追求无用的结果,一步一步向前迈进" <h2>从线索到复诊每一步都有明确承接人</h2>
align="center" </div>
data-aos="fade-up" <div class="process-slice__rail">
/> <article v-for="step in steps" :key="step.title" data-aos="fade-up">
<span>{{ step.index }}</span>
<div <h3>{{ step.title }}</h3>
class="max-w-4xl mx-auto mt-16" <p>{{ step.description }}</p>
data-aos="fade-up" </article>
data-aos-delay="200"
>
<div class="bg-white rounded-2xl shadow-lg overflow-hidden">
<div class="grid md:grid-cols-5">
<div class="md:col-span-3 p-8 md:p-12">
<h2 class="text-2xl font-bold text-gray-900 mb-6">我们的渐进式发展理念</h2>
<div class="space-y-6">
<div
class="flex"
data-aos="fade-right"
data-aos-delay="300"
>
<div class="flex-shrink-0">
<div class="w-12 h-12 rounded-lg bg-amber-50 flex items-center justify-center text-amber-600">
<i class="fas fa-seedling"></i>
</div>
</div>
<div class="ml-4">
<h3 class="text-lg font-medium text-gray-900">自然生长阶段</h3>
<p class="text-gray-600 mt-1">
在公司前期探索阶段我们遵循业务自然发展规律不揠苗助长
在探索中找准定位和发展方向
</p>
</div>
</div>
<div
class="flex"
data-aos="fade-right"
data-aos-delay="400"
>
<div class="flex-shrink-0">
<div class="w-12 h-12 rounded-lg bg-amber-50 flex items-center justify-center text-amber-600">
<i class="fas fa-stairs"></i>
</div>
</div>
<div class="ml-4">
<h3 class="text-lg font-medium text-gray-900">稳步前进阶段</h3>
<p class="text-gray-600 mt-1">
在明确方向后我们有规律有效率有目标地前进制定切实可行的阶段目标
一步一个脚印地实现突破
</p>
</div>
</div>
<div
class="flex"
data-aos="fade-right"
data-aos-delay="500"
>
<div class="flex-shrink-0">
<div class="w-12 h-12 rounded-lg bg-amber-50 flex items-center justify-center text-amber-600">
<i class="fas fa-mountain"></i>
</div>
</div>
<div class="ml-4">
<h3 class="text-lg font-medium text-gray-900">长期价值导向</h3>
<p class="text-gray-600 mt-1">
不追求短期利益最大化专注于创造可持续的长期价值
通过持续积累实现从量变到质变的飞跃
</p>
</div>
</div>
</div>
</div>
<div
class="md:col-span-2 bg-gradient-to-b from-amber-400 to-amber-600 p-8 flex items-center"
data-aos="fade-left"
data-aos-delay="600"
>
<div class="text-center w-full">
<div class="mb-6">
<i class="fas fa-chart-line text-4xl text-white opacity-80"></i>
</div>
<h3 class="text-xl font-bold text-white mb-2">渐进式发展曲线</h3>
<p class="text-amber-100 mb-6">持续积累阶梯式成长</p>
<div class="relative h-32">
<div class="absolute bottom-0 left-0 right-0">
<!-- 渐进式增长曲线示意 -->
<svg viewBox="0 0 200 100" class="w-full h-32">
<path
d="M0,80 C40,60 80,40 120,30 C160,20 200,10 200,10"
stroke="white"
stroke-width="3"
fill="none"
class="path-animation"
/>
<circle cx="0" cy="80" r="4" fill="white" />
<circle cx="40" cy="60" r="4" fill="white" />
<circle cx="80" cy="40" r="4" fill="white" />
<circle cx="120" cy="30" r="4" fill="white" />
<circle cx="160" cy="20" r="4" fill="white" />
<circle cx="200" cy="10" r="4" fill="white" />
</svg>
</div>
</div>
<p class="text-amber-100 text-sm mt-4">
每个阶段都是下一阶段的坚实基础
</p>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- 文化实践案例 --> <section class="editorial-section">
<section class="py-16 md:py-24">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<SectionTitle <div class="backend-panel" data-aos="zoom-in">
title="文化实践" <div>
subtitle="理念落地实例" <span class="eyebrow">PC 后台</span>
description="学习与渐进理念在实际工作中的体现" <h2>后台不是菜单集合而是权限内容和商业化的控制面</h2>
align="center" <p>页面模块素材招聘套餐订单角色菜单都由后台管理为后续数据库驱动和运营迭代留出空间</p>
data-aos="fade-up"
/>
<div
class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 mt-16"
data-aos="fade-up"
data-aos-delay="200"
>
<div
class="bg-white rounded-xl shadow-md overflow-hidden border border-gray-100"
data-aos="zoom-in"
data-aos-delay="300"
>
<div class="p-6">
<div class="w-12 h-12 rounded-lg bg-blue-50 flex items-center justify-center text-blue-600 mb-4">
<i class="fas fa-users"></i>
</div>
<h3 class="text-xl font-bold text-gray-800 mb-3">新人培养计划</h3>
<p class="text-gray-600 mb-4">
为期3个月的阶梯式培训体系从方法论到实践逐步培养行业研究能力
</p>
<div class="flex items-center text-sm text-gray-500">
<i class="far fa-clock mr-2"></i>
平均培养周期90
</div>
</div>
</div> </div>
<div class="backend-panel__preview">
<div <div v-for="item in backendItems" :key="item.title"><strong>{{ item.title }}</strong><span>{{ item.desc }}</span></div>
class="bg-white rounded-xl shadow-md overflow-hidden border border-gray-100"
data-aos="zoom-in"
data-aos-delay="400"
>
<div class="p-6">
<div class="w-12 h-12 rounded-lg bg-amber-50 flex items-center justify-center text-amber-600 mb-4">
<i class="fas fa-tasks"></i>
</div>
<h3 class="text-xl font-bold text-gray-800 mb-3">项目推进方法</h3>
<p class="text-gray-600 mb-4">
采用阶段性目标分解通过小步快跑的方式确保项目高质量交付
</p>
<div class="flex items-center text-sm text-gray-500">
<i class="fas fa-chart-line mr-2"></i>
项目成功率提升40%
</div>
</div>
</div>
<div
class="bg-white rounded-xl shadow-md overflow-hidden border border-gray-100"
data-aos="zoom-in"
data-aos-delay="500"
>
<div class="p-6">
<div class="w-12 h-12 rounded-lg bg-green-50 flex items-center justify-center text-green-600 mb-4">
<i class="fas fa-book-reader"></i>
</div>
<h3 class="text-xl font-bold text-gray-800 mb-3">知识迭代机制</h3>
<p class="text-gray-600 mb-4">
每季度更新行业研究方法论通过持续学习保持专业领先优势
</p>
<div class="flex items-center text-sm text-gray-500">
<i class="fas fa-book mr-2"></i>
已积累120+份研究文档
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 加入我们 -->
<section
class="py-16 md:py-24 bg-gradient-to-r from-blue-800 to-indigo-900"
data-aos="fade-up"
>
<div class="container mx-auto px-4">
<div
class="max-w-3xl mx-auto text-center"
data-aos="fade-up"
data-aos-delay="200"
>
<h2 class="text-3xl md:text-4xl font-bold text-white mb-6">加入酉佰共同成长</h2>
<p class="text-xl text-blue-200 mb-10">
我们正在寻找认同学习文化和渐进理念的伙伴一起创造长期价值
</p>
<div class="flex flex-col sm:flex-row justify-center gap-4">
<router-link
to="/careers"
class="px-8 py-4 bg-white text-blue-700 font-medium rounded-lg hover:bg-blue-50 transition-colors"
data-aos="zoom-in"
data-aos-delay="300"
>
查看职位空缺
<i class="fas fa-arrow-right ml-2"></i>
</router-link>
<router-link
to="/culture"
class="px-8 py-4 bg-transparent border-2 border-white text-white font-medium rounded-lg hover:bg-white/10 transition-colors"
data-aos="zoom-in"
data-aos-delay="400"
>
了解更多文化
</router-link>
</div> </div>
</div> </div>
</div> </div>
@@ -517,26 +74,29 @@
</template> </template>
<script setup> <script setup>
import SectionTitle from '@/components/ui/SectionTitle.vue' const capabilityNodes = [
{ label: '入口', title: '患者端', class: 'node-a' },
// 这里可以添加组件需要的响应式数据或方法 { label: '服务', title: '医生端', class: 'node-b' },
{ label: '增长', title: '推广员端', class: 'node-c' },
{ label: '判断', title: '老板端', class: 'node-d' },
{ label: '秩序', title: 'PC后台', class: 'node-core' }
]
const roles = [
{ index: '01', title: '患者端小程序', description: '让预约、提醒和服务记录留在患者触手可及的位置。', items: ['在线预约', '复诊提醒', '服务记录'] },
{ index: '02', title: '医生端小程序', description: '围绕接诊、随访和患者管理提升医生执行效率。', items: ['接诊记录', '患者管理', '随访任务'] },
{ index: '03', title: '门店老板端', description: '把线索、收入、转化和团队执行转为经营判断。', items: ['经营看板', '收入分析', '团队绩效'] },
{ index: '04', title: '推广员端', description: '让推广码、线索归属和成交转化可追踪。', items: ['推广码', '线索归属', '转化统计'] }
]
const steps = [
{ index: 'A', title: '获客', description: '官网、推广码和活动入口进入同一线索池。' },
{ index: 'B', title: '预约', description: '前台和医生看到待承接任务,减少遗漏。' },
{ index: 'C', title: '接诊', description: '服务过程沉淀为患者记录和后续动作。' },
{ index: 'D', title: '复诊', description: '提醒和随访任务把关系延续到离店之后。' },
{ index: 'E', title: '复盘', description: '老板端用数据看清获客和服务质量。' }
]
const backendItems = [
{ title: '内容 CMS', desc: '页面模块、图片、SEO/GSO' },
{ title: '商业化', desc: '套餐、订单、支付流水' },
{ title: '权限菜单', desc: '角色、多级菜单、布局偏好' }
]
</script> </script>
<style scoped>
.path-animation {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: dash 5s linear forwards;
}
@keyframes dash {
to {
stroke-dashoffset: 0;
}
}
/* 背景图片的透明度调整 */
.absolute img {
opacity: 0.65;
}
</style>

View File

@@ -1,284 +1,93 @@
<template> <template>
<div> <div class="page-shell rich-home">
<!-- 主视觉区域 --> <HeroCinematic />
<section class="pt-24 pb-16 md:pt-32 md:pb-24 bg-gradient-to-br from-blue-50 to-indigo-50">
<div class="container mx-auto px-4">
<div class="flex flex-col md:flex-row items-center">
<div class="md:w-1/2 mb-12 md:mb-0">
<h1
class="text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 mb-6 leading-tight"
data-aos="fade-up"
>
引领未来<br>
<span class="text-amber-600">创新</span>无限可能
</h1>
<p
class="text-lg text-gray-700 mb-8 max-w-lg"
data-aos="fade-up"
data-aos-delay="100"
>
上海酉佰致力于为企业提供前沿的解决方案赋能数字化转型
创造可持续的商业价值
</p>
<div
class="flex flex-wrap gap-4"
data-aos="fade-up"
data-aos-delay="200"
>
<router-link
to="/features"
class="px-6 py-3 bg-amber-600 hover:bg-amber-700 text-white rounded-lg font-medium shadow-md transition-all duration-300 hover:shadow-lg flex items-center"
>
<span>了解更多</span>
<i class="fas fa-arrow-right ml-2 text-xs"></i>
</router-link>
<button
class="px-6 py-3 bg-white text-indigo-900 border border-indigo-200 rounded-lg font-medium hover:bg-indigo-50 transition-colors flex items-center"
>
<i class="fas fa-phone-alt mr-2"></i>
<span>联系我们</span>
</button>
</div>
</div>
<div class="md:w-1/2 flex justify-center"> <section class="motion-section home-brief">
<div class="relative"> <div class="container mx-auto px-4">
<div class="absolute -top-6 -right-6 w-64 h-64 bg-amber-500 rounded-full opacity-10" data-aos="fade"></div> <div class="home-brief__grid">
<div class="absolute -bottom-6 -left-6 w-72 h-72 bg-indigo-500 rounded-full opacity-10" data-aos="fade" data-aos-delay="300"></div> <div data-aos="fade-right">
<div <span class="eyebrow">官网首页</span>
class="relative z-10 rounded-2xl overflow-hidden shadow-xl w-full max-w-md" <h2>不重复讲功能先让经营者看见系统进入诊所的方式</h2>
data-aos="zoom-in" <p>首页负责建立第一层判断萧康云医能把获客服务复诊和经营复盘连起来具体模块阶段方案和价格交给各自页面展开</p>
data-aos-delay="200" </div>
> <div class="home-brief__metrics">
<!-- 图片占位 --> <MetricArtCard v-for="metric in metrics" :key="metric.label" v-bind="metric" />
<div class="bg-gradient-to-br from-indigo-600 to-indigo-900 border-2 border-dashed rounded-xl w-full h-96 p-6 flex items-center justify-center">
<div class="text-center text-white">
<i class="fas fa-chart-line text-5xl mb-4 opacity-70"></i>
<p class="text-xl font-light">企业解决方案展示</p>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- 核心价值 --> <ClinicJourney />
<section class="py-16 md:py-24"> <RoleConstellation />
<div class="container mx-auto px-4"> <DataCurrent />
<SectionTitle <ScenarioShowcase />
title="我们的核心价值"
subtitle="以专业赢得信任"
description="我们坚持创新驱动与客户至上的理念,为企业提供专业的数字化转型解决方案"
align="center"
data-aos="fade-up"
/>
<div class="grid md:grid-cols-3 gap-8 mt-16"> <section class="motion-section">
<Card <div class="container mx-auto px-4">
v-for="(value, index) in values" <div class="section-intro" data-aos="fade-up">
:key="index" <span class="eyebrow">首页分工</span>
:title="value.title" <h2>把信息拆到正确的位置用户才不会在首页迷路</h2>
:icon="value.icon" </div>
:data-aos="'fade-up'" <div class="editorial-card-grid">
:data-aos-delay="index * 100" <EditorialCard
v-for="card in pageGuides"
:key="card.title"
v-bind="card"
> >
<p> <router-link :to="card.link" class="text-sm font-bold text-[var(--gold)]">进入页面</router-link>
{{ value.description }} </EditorialCard>
</p>
<template #actions>
<a
href="#"
class="inline-flex items-center mt-4 font-medium text-amber-600 hover:text-amber-700 transition-colors"
>
<span>了解详情</span>
<i class="fas fa-arrow-right ml-2 text-xs"></i>
</a>
</template>
</Card>
</div> </div>
</div> </div>
</section> </section>
<!-- 企业优势 --> <section class="motion-section editorial-muted">
<section class="py-16 md:py-24 bg-gray-50">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex flex-col lg:flex-row items-center justify-between"> <div class="home-flow">
<div class="lg:w-5/12 mb-12 lg:mb-0"> <div data-aos="fade-right">
<SectionTitle <span class="eyebrow">上线路径</span>
title="为什么选择" <h2>从一张官网线索到一套可运营的系统</h2>
subtitle="企业优势" <p>按真实门店节奏落地而不是一次性把所有功能塞给团队</p>
highlight="上海酉佰?"
data-aos="fade-right"
/>
<p class="text-gray-600 mt-6 mb-8" data-aos="fade-right" data-aos-delay="100">
我们拥有多年的行业经验和专业团队致力于为客户提供全方位的解决方案
凭借独特的技术优势和创新的服务理念帮助企业实现数字化转型
</p>
<div class="grid grid-cols-2 gap-4">
<div
v-for="(stat, index) in stats"
:key="index"
class="bg-white rounded-lg p-4 flex flex-col items-center"
:data-aos="'fade-up'"
:data-aos-delay="(index * 100) + 200"
>
<div class="text-3xl font-bold text-indigo-800">{{ stat.value }}</div>
<div class="text-gray-600">{{ stat.label }}</div>
</div>
</div>
</div> </div>
<ol>
<div class="lg:w-6/12" data-aos="fade-left" data-aos-delay="200"> <li v-for="item in rollout" :key="item.title" data-aos="fade-up">
<div class="relative"> <span>{{ item.index }}</span>
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-full h-full rounded-3xl bg-gradient-to-r from-amber-400/10 to-blue-400/10 rotate-12"></div> <div>
<div class="relative bg-white rounded-2xl overflow-hidden shadow-lg flex"> <h3>{{ item.title }}</h3>
<div class="hidden lg:block w-1/3 bg-gradient-to-b from-amber-500 to-amber-600 p-6 flex items-center justify-center bg-cover bg-center" <p>{{ item.description }}</p>
style="background-image: url('https://images.unsplash.com/photo-1544005313-94ddf0286df2?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80')">
<i class="fas fa-medal text-amber-600 text-5xl"></i>
</div>
<div class="w-full lg:w-2/3 p-8">
<h3 class="text-xl font-bold text-gray-900 mb-4">专业服务能力</h3>
<ul class="space-y-4">
<li
v-for="(item, index) in advantages"
:key="index"
class="flex items-start"
:data-aos="'fade-up'"
:data-aos-delay="index * 100"
>
<div class="w-8 h-8 rounded-full bg-amber-50 text-amber-600 flex items-center justify-center flex-shrink-0 mr-3">
<i :class="item.icon"></i>
</div>
<div>
<h4 class="font-medium text-gray-900">{{ item.title }}</h4>
<p class="text-gray-600 text-sm mt-1">{{ item.description }}</p>
</div>
</li>
</ul>
</div>
</div> </div>
</div> </li>
</ol>
</div>
</div>
</section>
<section class="motion-section">
<div class="container mx-auto px-4">
<div class="faq-composer">
<div data-aos="fade-right">
<span class="eyebrow">常见问题</span>
<h2>先回答经营者真正会问的事</h2>
</div>
<div class="faq-list" data-aos="fade-left">
<details v-for="faq in faqs" :key="faq.question" open>
<summary>{{ faq.question }}</summary>
<p>{{ faq.answer }}</p>
</details>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- 客户评价 --> <section class="motion-section final-cta">
<section class="py-16 md:py-24">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<SectionTitle <div class="final-cta__inner" data-aos="zoom-in">
title="客户评价" <div>
subtitle="他们这样说" <span class="eyebrow">下一步</span>
description="多年来,我们服务的客户遍布不同行业,获得了广泛认可" <h2>先看一次真实流程再决定萧康云医如何进入你的诊所</h2>
align="center"
data-aos="fade-up"
/>
<div class="grid md:grid-cols-3 gap-8 mt-16">
<div
v-for="(testimonial, index) in testimonials"
:key="index"
:data-aos="'fade-up'"
:data-aos-delay="index * 100"
>
<Card size="lg" hover-effect no-border class="h-full" icon-bg-class="bg-indigo-50 text-indigo-600">
<template #icon>
<div class="text-5xl text-indigo-100 font-serif">"</div>
</template>
<p class="text-gray-600 italic">
{{ testimonial.content }}
</p>
<div class="mt-6 flex items-center">
<div class="w-12 h-12 rounded-full overflow-hidden mr-4">
<img :src="testimonial.avatar" alt="" />
</div>
<div>
<div class="font-medium text-gray-900">{{ testimonial.name }}</div>
<div class="text-sm text-gray-500">{{ testimonial.position }}</div>
</div>
</div>
</Card>
</div> </div>
</div> <div class="flex flex-col gap-3 sm:flex-row">
</div> <router-link to="/pricing" class="primary-button justify-center">预约演示</router-link>
</section> <router-link to="/solutions" class="secondary-button justify-center">查看方案</router-link>
<!-- 合作品牌 -->
<section class="py-16 md:py-20 bg-gradient-to-r from-indigo-50 to-blue-50 overflow-hidden">
<div class="container mx-auto px-4">
<SectionTitle
title="合作伙伴"
subtitle="信赖品牌"
align="center"
data-aos="fade-up"
/>
<div class="partners-container mt-12 bg-white">
<div
v-for="(row, rowIndex) in partnerRows"
:key="rowIndex"
class="marquee-row-wrapper mb-8"
@mouseenter="pauseRow(rowIndex)"
@mouseleave="resumeRow(rowIndex)"
>
<div
:class="[
'marquee-row',
rowIndex % 2 === 0 ? 'scroll-left' : 'scroll-right',
rowIndex % 3 === 0 ? 'scroll-slow' : rowIndex % 3 === 1 ? 'scroll-medium' : 'scroll-fast'
]"
:data-row="rowIndex"
>
<div
v-for="(partner, partnerIndex) in row"
:key="partnerIndex"
class="partner-item"
>
<div class="partner-logo-container">
<img :src="partner.logo" alt="" />
<div class="partner-name">{{ partner.name }}</div>
</div>
</div>
<div
v-for="(partner, partnerIndex) in row"
:key="partnerIndex + 'copy'"
class="partner-item"
>
<div class="partner-logo-container">
<img :src="partner.logo" alt="" />
<div class="partner-name">{{ partner.name }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="py-16 md:py-24 bg-gradient-to-r from-indigo-900 to-blue-900 text-white">
<div class="container mx-auto px-4">
<div class="max-w-3xl mx-auto text-center" data-aos="zoom-in">
<h2 class="text-3xl md:text-4xl font-bold mb-6">开启数字化转型之旅</h2>
<p class="text-xl text-blue-200 mb-8">
联系我们获取免费咨询服务,量身定制您的解决方案
</p>
<div class="flex flex-col sm:flex-row justify-center gap-4">
<button class="px-8 py-3.5 bg-amber-500 hover:bg-amber-600 rounded-lg font-medium shadow-lg transition-colors duration-300 flex items-center justify-center">
立即咨询
<i class="fas fa-comment ml-3"></i>
</button>
<button class="px-8 py-3.5 bg-transparent border-2 border-white/30 hover:border-white rounded-lg font-medium flex items-center justify-center">
预约演示
<i class="fas fa-calendar ml-3"></i>
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -287,324 +96,36 @@
</template> </template>
<script setup> <script setup>
import SectionTitle from '@/components/ui/SectionTitle.vue' import HeroCinematic from '@/components/home/HeroCinematic.vue'
import Card from '@/components/ui/Card.vue' import ClinicJourney from '@/components/home/ClinicJourney.vue'
import {computed, onMounted, ref} from "vue"; import RoleConstellation from '@/components/home/RoleConstellation.vue'
import DataCurrent from '@/components/home/DataCurrent.vue'
import ScenarioShowcase from '@/components/home/ScenarioShowcase.vue'
import EditorialCard from '@/components/ui/EditorialCard.vue'
import MetricArtCard from '@/components/ui/MetricArtCard.vue'
const values = [ const metrics = [
{ { label: '第一层', value: '入口', description: '官网只负责让诊所愿意开始对话。' },
title: '创新驱动', { label: '第二层', value: '流程', description: '把线索、预约、接诊和复诊串起来。' },
icon: 'fas fa-lightbulb', { label: '第三层', value: '判断', description: '让老板看到经营动作背后的结果。' }
description: '不断探索新技术、新思路,为客户提供前沿解决方案'
},
{
title: '客户至上',
icon: 'fas fa-handshake',
description: '深度理解客户需求,提供定制化服务与全流程支持'
},
{
title: '专业可靠',
icon: 'fas fa-medal',
description: '高素质专业团队,严谨的工作态度,确保项目高质量交付'
}
] ]
const stats = [ const pageGuides = [
{ value: '12+', label: '行业经验' }, { index: '01', eyebrow: '产品能力', title: '看模块如何协同', description: '适合已经理解需求、想确认系统边界的人。', link: '/features', tone: 'paper' },
{ value: '200+', label: '合作企业' }, { index: '02', eyebrow: '解决方案', title: '看阶段如何落地', description: '适合单店起步、增长运营和连锁预留的不同诊所。', link: '/solutions', tone: 'line' },
{ value: '50+', label: '专业团队' }, { index: '03', eyebrow: '价格', title: '看投入如何选择', description: '适合需要比较月付、年付和套餐权益的人。', link: '/pricing', tone: 'warm' }
{ value: '98%', label: '客户满意度' }
] ]
const advantages = [ const rollout = [
{ { index: '01', title: '梳理经营入口', description: '确认官网、推广码、电话咨询和到店登记如何进入同一套线索池。' },
title: '行业专业知识', { index: '02', title: '配置角色端口', description: '按诊所团队结构启用患者端、医生端、老板端、推广员端和 PC 后台。' },
icon: 'fas fa-book', { index: '03', title: '跑通服务闭环', description: '让预约、接诊、随访和复诊提醒先在真实门店流程里顺起来。' },
description: '深耕行业多年的专家团队,掌握行业发展趋势' { index: '04', title: '沉淀经营判断', description: '用线索来源、转化、复购和订单数据帮助老板做每周经营复盘。' }
},
{
title: '技术创新',
icon: 'fas fa-cogs',
description: '持续投资技术研发,引领行业技术创新'
},
{
title: '定制解决方案',
icon: 'fas fa-clipboard-list',
description: '根据企业需求量身定制解决方案'
},
] ]
const testimonials = [ const faqs = [
{ { question: '首页为什么不把所有功能一次讲完?', answer: '首页负责建立理解和信任,具体模块能力、阶段方案和价格分别进入产品能力页、解决方案页和定价页。' },
content: '上海酉佰团队的专业性和创新思维帮助我们完成了数字化转型,大幅提升了工作效率,为业务增长提供了强大支持。', { question: '后台是否已经有前端入口?', answer: '后台使用 /admin 分组路由,登录后进入仪表盘,并从后端菜单接口渲染多级菜单。' },
name: '张先生', { question: '动效会影响移动端性能吗?', answer: '动效以 CSS、SVG、AOS 和 ECharts 轻量实现,并提供 reduced-motion 降级。' }
position: '某公司总经理',
avatar: 'https://randomuser.me/api/portraits/men/32.jpg'
},
{
content: '与酉佰的合作非常愉快,他们的解决方案深入理解了我们行业的特殊需求,实施效果远超我们的预期。',
name: '李女士',
position: '某制造企业CIO',
avatar: 'https://randomuser.me/api/portraits/women/44.jpg'
},
{
content: '专业、高效、值得信赖的技术合作伙伴,他们的团队总能提出创新的解决方案帮助解决业务痛点。',
name: '王总监',
position: '某零售企业技术总监',
avatar: 'https://randomuser.me/api/portraits/men/67.jpg'
}
] ]
// 生成合作伙伴数据(实际项目应使用真实数据)
const partners = ref([
{ name: '腾讯科技', logo: 'https://logo.clearbit.com/tencent.com' },
{ name: '阿里巴巴', logo: 'https://logo.clearbit.com/alibaba.com' },
{ name: '华为技术', logo: 'https://logo.clearbit.com/huawei.com' },
{ name: '字节跳动', logo: 'https://logo.clearbit.com/bytedance.com' },
{ name: '京东集团', logo: 'https://logo.clearbit.com/jd.com' },
{ name: '百度科技', logo: 'https://logo.clearbit.com/baidu.com' },
{ name: '美团点评', logo: 'https://logo.clearbit.com/meituan.com' },
{ name: '拼多多', logo: 'https://logo.clearbit.com/pinduoduo.com' },
{ name: '小米科技', logo: 'https://logo.clearbit.com/mi.com' },
{ name: '网易科技', logo: 'https://logo.clearbit.com/netease.com' },
{ name: '携程旅行', logo: 'https://logo.clearbit.com/ctrip.com' },
{ name: '中国平安', logo: 'https://logo.clearbit.com/pingan.com' },
{ name: '招商银行', logo: 'https://logo.clearbit.com/cmbchina.com' },
{ name: '中国移动', logo: 'https://logo.clearbit.com/chinamobileltd.com' },
{ name: '中国电信', logo: 'https://logo.clearbit.com/chinatelecom-h.com' },
{ name: '格力电器', logo: 'https://logo.clearbit.com/gree.com' },
{ name: '海尔智家', logo: 'https://tse3-mm.cn.bing.net/th/id/OIP-C.FWv9RNjTfGRu1-9fXWCslAHaFj?rs=1&pid=ImgDetMain' },
{ name: '联想集团', logo: 'https://logo.clearbit.com/lenovo.com' },
{ name: '比亚迪汽车', logo: 'https://logo.clearbit.com/byd.com' },
{ name: '上汽集团', logo: 'https://logo.clearbit.com/saicmotor.com' },
{ name: '万科集团', logo: 'https://logo.clearbit.com/vanke.com' },
{ name: '碧桂园', logo: 'https://logo.clearbit.com/countrygarden.com.cn' },
{ name: '中国建筑', logo: 'https://tse1-mm.cn.bing.net/th/id/OIP-C.Y69rDHEetAyFALkEm8UnEwHaEU?w=318&h=185&c=7&r=0&o=7&pid=1.7&rm=3' },
{ name: '中信集团', logo: 'https://logo.clearbit.com/citic.com' },
{ name: '中国石油', logo: 'https://logo.clearbit.com/petrochina.com.cn' },
{ name: '中国石化', logo: 'https://logo.clearbit.com/sinopec.com' },
{ name: '国家电网', logo: 'https://www.logobiaozhi.com/upLoad/image/20240118/17055583781209531.jpg' },
{ name: '中国工商银行', logo: 'https://logo.clearbit.com/icbc.com.cn' },
{ name: '建设银行', logo: 'https://logo.clearbit.com/ccb.com' },
{ name: '农业银行', logo: 'https://logo.clearbit.com/abchina.com' }
])
// 将合作伙伴按每行10个分组
const partnerRows = computed(() => {
const rows = []
const itemsPerRow = 10
for (let i = 0; i < partners.value.length; i += itemsPerRow) {
rows.push(partners.value.slice(i, i + itemsPerRow))
}
return rows
})
// 暂停指定行滚动
const pauseRow = (rowIndex) => {
const rowElements = document.querySelectorAll(`.marquee-row[data-row="${rowIndex}"]`)
rowElements.forEach(el => {
el.style.animationPlayState = 'paused'
})
}
// 恢复指定行滚动
const resumeRow = (rowIndex) => {
const rowElements = document.querySelectorAll(`.marquee-row[data-row="${rowIndex}"]`)
rowElements.forEach(el => {
el.style.animationPlayState = 'running'
})
}
// 初始化滚动效果
onMounted(() => {
const style = document.createElement('style')
style.textContent = `
@keyframes scroll-left {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
@keyframes scroll-right {
0% { transform: translateX(-50%); }
100% { transform: translateX(0); }
}
.scroll-slow {
animation: scroll-left 40s linear infinite;
}
.scroll-medium {
animation: scroll-left 30s linear infinite;
}
.scroll-fast {
animation: scroll-left 20s linear infinite;
}
.marquee-row:hover {
animation-play-state: paused;
}
`
document.head.appendChild(style)
})
</script> </script>
<style scoped>
/* 合作伙伴容器 */
.partners-container {
max-width: 1800px;
margin: 0 auto;
border-radius: 12px;
padding: 5px 0;
}
/* 行容器 */
.marquee-row-wrapper {
overflow: hidden;
position: relative;
padding: 12px 0;
border-radius: 12px;
background: rgba(255, 255, 255, 0.6);
/*box-shadow: 0 4px 6px rgba(79, 70, 229, 0.05);*/
transition: all 0.3s ease;
}
.marquee-row-wrapper:hover {
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 10px 15px rgba(79, 70, 229, 0.1);
}
/* 行内容 */
.marquee-row {
display: flex;
width: max-content;
}
/* 合作伙伴项目 */
.partner-item {
flex-shrink: 0;
padding: 0 20px;
}
/* 合作伙伴logo容器 */
.partner-logo-container {
position: relative;
width: 140px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
border-radius: 8px;
padding: 10px;
background: white;
}
/*.partner-logo-container:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
}*/
/* 合作伙伴logo */
.partner-logo {
max-height: 50px;
max-width: 120px;
object-fit: contain;
transition: all 0.3s ease;
filter: grayscale(0);
}
/* 鼠标悬停时logo变黑 */
/*.partner-logo-container:hover .partner-logo {
filter: grayscale(100%) brightness(0.3);
}*/
/* 合作伙伴名称 */
.partner-name {
position: absolute;
bottom: -25px;
left: 0;
right: 0;
text-align: center;
font-size: 12px;
color: #4f46e5;
background: white;
padding: 2px 5px;
border-radius: 4px;
opacity: 0;
transform: translateY(5px);
transition: all 0.3s ease;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
/* 鼠标悬停时显示名称 */
/*.partner-logo-container:hover .partner-name {
opacity: 1;
transform: translateY(0);
}*/
/* 滚动动画 */
@keyframes scroll-left {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
@keyframes scroll-right {
0% { transform: translateX(-50%); }
100% { transform: translateX(0); }
}
.scroll-slow {
animation: scroll-left 40s linear infinite;
}
.scroll-medium {
animation: scroll-left 30s linear infinite;
}
.scroll-fast {
animation: scroll-left 20s linear infinite;
}
.scroll-left {
animation: scroll-left 40s linear infinite;
}
.scroll-right {
animation: scroll-right 40s linear infinite;
}
.marquee-row:hover {
animation-play-state: paused;
}
/* 响应式调整 */
@media (max-width: 1024px) {
.partner-item {
padding: 0 15px;
}
.partner-logo-container {
width: 120px;
}
}
@media (max-width: 768px) {
.marquee-row-wrapper {
margin-bottom: 12px;
}
.partner-item {
padding: 0 10px;
}
.partner-logo-container {
width: 100px;
height: 70px;
}
.partner-logo {
max-height: 40px;
}
}
</style>

108
src/views/Pricing.vue Normal file
View File

@@ -0,0 +1,108 @@
<template>
<div class="page-shell refined-page pricing-refined">
<section class="subpage-hero pricing-hero-refined">
<div class="container mx-auto px-4">
<div class="subpage-hero__grid">
<div data-aos="fade-up">
<span class="subpage-kicker">PRICING</span>
<h1>按诊所阶段选择套餐而不是被功能清单淹没</h1>
<p>价格页专注回答三件事当前阶段选哪档月付年付差异提交后如何进入演示或订购流程</p>
</div>
<div class="pricing-switch-card" data-aos="fade-left">
<span>计费周期</span>
<div>
<button :class="{ active: billingCycle === 'month' }" type="button" @click="billingCycle = 'month'">月付</button>
<button :class="{ active: billingCycle === 'year' }" type="button" @click="billingCycle = 'year'">年付</button>
</div>
<p>年付适合已确定上线计划的诊所便于统一部署和运营辅导</p>
</div>
</div>
</div>
</section>
<section class="editorial-section">
<div class="container mx-auto px-4">
<div class="pricing-cards-v2">
<article v-for="plan in plans" :key="plan.code" :class="{ recommended: plan.recommended }" data-aos="fade-up">
<div class="pricing-card-head">
<span>{{ plan.stage }}</span>
<h2>{{ plan.name }}</h2>
<p>{{ plan.description }}</p>
</div>
<div class="pricing-card-price">
<strong>{{ formatPrice(plan) }}</strong><small>/{{ billingCycle === 'month' ? '月' : '年' }}</small>
</div>
<ul><li v-for="feature in plan.features" :key="feature.name"><span>{{ feature.name }}</span>{{ feature.value }}</li></ul>
<button :class="plan.recommended ? 'primary-button' : 'secondary-button'" type="button" @click="selectPlan(plan)">选择{{ plan.name }}</button>
</article>
</div>
</div>
</section>
<section class="editorial-section editorial-muted">
<div class="container mx-auto px-4">
<div class="feature-compare" data-aos="fade-up">
<div>
<span class="eyebrow">权益矩阵</span>
<h2>核心能力怎么分布一张表看清</h2>
</div>
<div class="compare-table">
<div class="compare-row compare-head"><span>能力</span><strong v-for="plan in plans" :key="plan.code">{{ plan.name }}</strong></div>
<div v-for="row in compareRows" :key="row.name" class="compare-row"><span>{{ row.name }}</span><strong v-for="plan in plans" :key="plan.code">{{ row[plan.code] }}</strong></div>
</div>
</div>
</div>
</section>
<section class="editorial-section">
<div class="container mx-auto px-4">
<div class="order-panel order-panel-v2" id="order-form" data-aos="fade-up">
<div>
<span class="eyebrow">订购与演示</span>
<h2>留下诊所信息我们把方案讲清楚</h2>
<p>当前选择{{ selectedPlan.name }} · {{ billingCycle === 'month' ? '月付' : '年付' }}提交后运营会联系你确认演示和支付方式</p>
</div>
<form @submit.prevent="submitOrder">
<label><span>诊所名称</span><input v-model="form.tenant_name" class="form-input" autocomplete="organization" /></label>
<label><span>联系人</span><input v-model="form.contact_name" class="form-input" autocomplete="name" /></label>
<label><span>手机号</span><input v-model="form.phone" class="form-input" type="tel" autocomplete="tel" /></label>
<label><span>所在城市</span><input v-model="form.city" class="form-input" autocomplete="address-level2" /></label>
<label class="full"><span>补充说明</span><textarea v-model="form.remark" class="form-input" rows="4" placeholder="门店数量、当前获客方式、期望上线时间"></textarea></label>
<div class="form-actions full">
<button class="primary-button" type="submit"><i class="fas fa-file-invoice"></i>创建订购订单</button>
<button class="secondary-button" type="button" @click="submitLead"><i class="fas fa-headset"></i>只预约演示</button>
</div>
</form>
<div v-if="message" class="pricing-message" aria-live="polite">{{ message }}</div>
</div>
</div>
</section>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { post } from '@/api/request'
const billingCycle = ref('month')
const message = ref('')
const plans = [
{ code: 'starter', stage: '单店起步', name: '基础版', description: '适合先把预约、线索和基础后台跑起来的单店诊所。', monthly_fee: 19900, yearly_fee: 199000, recommended: false, features: [{ name: '患者端', value: '基础预约与提醒' }, { name: 'PC后台', value: '线索与订单管理' }, { name: '账号数量', value: '5个员工账号' }] },
{ code: 'growth', stage: '增长运营', name: '增长版', description: '适合需要推广获客、医生协同和经营分析的成长型诊所。', monthly_fee: 39900, yearly_fee: 399000, recommended: true, features: [{ name: '四端小程序', value: '患者/医生/老板/推广员' }, { name: '经营看板', value: '转化、收入、复购分析' }, { name: '账号数量', value: '20个员工账号' }] },
{ code: 'enterprise', stage: '连锁预留', name: '连锁版', description: '适合多门店和需要深度配置的诊所集团。', monthly_fee: 89900, yearly_fee: 899000, recommended: false, features: [{ name: '多门店', value: '预留扩展与独立配置' }, { name: '专属服务', value: '部署辅导和运营顾问' }, { name: '账号数量', value: '不限账号数量' }] }
]
const compareRows = [
{ name: '患者端小程序', starter: '基础', growth: '完整', enterprise: '完整' },
{ name: '医生/老板/推广员端', starter: '—', growth: '包含', enterprise: '包含' },
{ name: '经营数据分析', starter: '基础', growth: '完整', enterprise: '定制' },
{ name: '多门店能力', starter: '—', growth: '预留', enterprise: '包含' },
{ name: '上线辅导', starter: '标准', growth: '增强', enterprise: '专属' }
]
const selectedPlan = ref(plans[1])
const form = ref({ tenant_name: '', contact_name: '', phone: '', city: '', remark: '' })
const selectedAmount = computed(() => billingCycle.value === 'year' ? selectedPlan.value.yearly_fee : selectedPlan.value.monthly_fee)
const formatPrice = (plan) => `¥${((billingCycle.value === 'year' ? plan.yearly_fee : plan.monthly_fee) / 100).toLocaleString('zh-CN')}`
const selectPlan = (plan) => { selectedPlan.value = plan; document.getElementById('order-form')?.scrollIntoView({ behavior: 'smooth', block: 'start' }) }
const submitOrder = async () => { message.value = ''; await post('/orders', { ...form.value, plan_code: selectedPlan.value.code, billing_cycle: billingCycle.value, amount: selectedAmount.value }); message.value = '订购订单已提交,后台会生成待支付订单并联系你确认支付方式。' }
const submitLead = async () => { message.value = ''; await post('/leads', { name: form.value.contact_name, phone: form.value.phone, clinic_name: form.value.tenant_name, city: form.value.city, plan_code: selectedPlan.value.code, source_page: 'pricing', remark: form.value.remark }); message.value = '预约演示已提交,我们会尽快联系你。' }
</script>

56
src/views/Solutions.vue Normal file
View File

@@ -0,0 +1,56 @@
<template>
<div class="page-shell">
<section class="art-hero min-h-[620px]">
<img class="art-hero-media" src="@/assets/images/company0005.jpeg" alt="萧康云医解决方案" />
<div class="art-hero-content">
<div class="container mx-auto px-4">
<div class="max-w-4xl" data-aos="fade-up">
<div class="art-kicker">CLINIC GROWTH PATH</div>
<h1 class="mt-6 text-4xl font-bold leading-tight md:text-6xl">不同阶段的诊所需要不同的系统进入方式</h1>
<p class="mt-6 max-w-2xl text-lg leading-9 text-stone-100">解决方案页只讲落地阶段先线上化再做增长运营最后为连锁和权限扩展预留秩序</p>
</div>
</div>
</div>
</section>
<section class="editorial-section">
<div class="container mx-auto px-4">
<div class="solution-path">
<ScenarioImageCard
v-for="(solution, index) in solutions"
:key="solution.title"
:reverse="index % 2 === 1"
v-bind="solution"
>
<div class="mt-6 flex flex-wrap gap-2">
<span v-for="item in solution.items" :key="item" class="rounded-full border border-[var(--line)] px-3 py-1 text-xs text-[var(--text-muted)]">{{ item }}</span>
</div>
</ScenarioImageCard>
</div>
</div>
</section>
<section class="editorial-section editorial-muted">
<div class="container mx-auto px-4">
<div class="stage-note" data-aos="zoom-in">
<span class="eyebrow">阶段选择</span>
<h2>不是每家诊所都要一步到位但每一步都要能接住下一步</h2>
<router-link to="/pricing" class="primary-button justify-center">查看对应套餐</router-link>
</div>
</div>
</section>
</div>
</template>
<script setup>
import ScenarioImageCard from '@/components/ui/ScenarioImageCard.vue'
import company2 from '@/assets/images/company0002.jpeg'
import company3 from '@/assets/images/company0003.jpeg'
import company5 from '@/assets/images/company0005.jpeg'
const solutions = [
{ label: '起步期', title: '单店诊所线上化', description: '快速搭建预约、线索和基础后台,让诊所从手工登记切换到线上承接。', items: ['患者端预约入口', 'PC 线索管理', '基础套餐订购'], image: company2 },
{ label: '增长期', title: '推广获客与复诊运营', description: '为推广员、医生和老板建立协同流程,提升线索转化和复诊效率。', items: ['推广员线索归属', '医生随访任务', '老板经营看板'], image: company3 },
{ label: '扩展期', title: '连锁门店管理预留', description: '为多门店、更多角色和深度权限管理预留后台数据结构与菜单能力。', items: ['多级菜单权限', '订阅和订单闭环', '租户主体扩展'], image: company5 }
]
</script>

View File

@@ -89,7 +89,7 @@
<div class="md:w-2/3 md:pl-8 order-2 lg:order-2" data-aos="fade-right" data-aos-delay="200"> <div class="md:w-2/3 md:pl-8 order-2 lg:order-2" data-aos="fade-right" data-aos-delay="200">
<h3 class="text-2xl font-bold text-gray-900 mb-4">技术专家团队</h3> <h3 class="text-2xl font-bold text-gray-900 mb-4">技术专家团队</h3>
<p class="text-gray-600 mb-6"> <p class="text-gray-600 mb-6">
酉佰佰是一支秉承着"开放透明、纪律严明、听从指挥、主观能动"管理风格的团队 萧康云医是一支秉承着"开放透明、纪律严明、听从指挥、主观能动"管理风格的团队
我们遵循循序渐进但绝不放任自流的管理理念 我们遵循循序渐进但绝不放任自流的管理理念
</p> </p>
<p class="text-gray-600 mb-6"> <p class="text-gray-600 mb-6">
@@ -118,7 +118,7 @@
<SectionTitle <SectionTitle
title="发展规划" title="发展规划"
subtitle="未来愿景" subtitle="未来愿景"
description="酉佰佰团队的战略发展规划" description="萧康云医团队的战略发展规划"
align="center" align="center"
data-aos="fade-up" data-aos="fade-up"
/> />
@@ -132,7 +132,7 @@
<div class="md:w-2/3 md:pl-8 order-2 lg:order-1" data-aos="fade-left" data-aos-delay="200"> <div class="md:w-2/3 md:pl-8 order-2 lg:order-1" data-aos="fade-left" data-aos-delay="200">
<h3 class="text-2xl font-bold text-gray-900 mb-4">五年发展规划</h3> <h3 class="text-2xl font-bold text-gray-900 mb-4">五年发展规划</h3>
<p class="text-gray-600 mb-6"> <p class="text-gray-600 mb-6">
酉佰佰团队刚经历了第一个五年的发展展望未来公司一方面致力于在宏观弱势期进行人才积累 萧康云医团队刚经历了第一个阶段的发展展望未来公司一方面致力于在医疗数字化场景中进行人才积累
另一方面也在积极地进行政治经济研究产业企业研究风险管理研究和投资等投资公司核心竞争力的孵化与锻炼 另一方面也在积极地进行政治经济研究产业企业研究风险管理研究和投资等投资公司核心竞争力的孵化与锻炼
</p> </p>
<p class="text-gray-600 mb-6"> <p class="text-gray-600 mb-6">
@@ -254,7 +254,7 @@
<div class="max-w-3xl mx-auto text-center" data-aos="fade-up"> <div class="max-w-3xl mx-auto text-center" data-aos="fade-up">
<h2 class="text-3xl md:text-4xl font-bold mb-6">加入我们的团队</h2> <h2 class="text-3xl md:text-4xl font-bold mb-6">加入我们的团队</h2>
<p class="text-xl text-blue-300 mb-8"> <p class="text-xl text-blue-300 mb-8">
如果你才华横溢充满激情渴望成长我们欢迎你加入酉佰佰大家庭 如果你才华横溢充满激情渴望成长我们欢迎你加入萧康云医大家庭
</p> </p>
<router-link <router-link
to="/careers" to="/careers"

View File

@@ -0,0 +1,73 @@
<template>
<div class="admin-dashboard-v2">
<section class="admin-dashboard-hero">
<div>
<span class="admin-eyebrow">TODAY</span>
<h2>运营控制台</h2>
<p>把官网线索套餐订购待支付订单和菜单权限放在同一个工作面板里运营不用在页面之间反复迷路</p>
</div>
<div class="admin-dashboard-hero__action">
<button class="primary-button admin-primary-action" type="button">处理今日线索</button>
<button class="admin-button" type="button">导出周报</button>
</div>
</section>
<section class="admin-stat-grid admin-stat-grid-v2">
<article v-for="stat in stats" :key="stat.label" class="admin-stat-card admin-stat-card-v2">
<i :class="stat.icon"></i>
<span>{{ stat.label }}</span>
<strong>{{ stat.value }}</strong>
<small>{{ stat.note }}</small>
</article>
</section>
<section class="admin-dashboard-grid">
<div class="admin-board admin-board-v2">
<div class="admin-board__head">
<div><span class="admin-eyebrow">PIPELINE</span><h3>线索处理节奏</h3></div>
<button class="admin-button" type="button">查看全部</button>
</div>
<div class="admin-pipeline">
<div v-for="item in pipeline" :key="item.name">
<span>{{ item.name }}</span>
<strong>{{ item.value }}</strong>
<i :style="{ width: item.width }"></i>
</div>
</div>
</div>
<div class="admin-board admin-board-v2">
<div class="admin-board__head">
<div><span class="admin-eyebrow">TASKS</span><h3>待办动作</h3></div>
</div>
<ul class="admin-task-list">
<li v-for="task in tasks" :key="task.title">
<span :class="task.level"></span>
<div><strong>{{ task.title }}</strong><p>{{ task.desc }}</p></div>
<button type="button">处理</button>
</li>
</ul>
</div>
</section>
</div>
</template>
<script setup>
const stats = [
{ label: '待跟进线索', value: '18', note: '+6 今日新增', icon: 'fas fa-message' },
{ label: '套餐数量', value: '3', note: '基础/增长/连锁', icon: 'fas fa-box-open' },
{ label: '待支付订单', value: '7', note: '需运营确认', icon: 'fas fa-receipt' },
{ label: '菜单节点', value: '11', note: '支持单列/双列', icon: 'fas fa-bars-staggered' }
]
const pipeline = [
{ name: '新线索', value: 18, width: '78%' },
{ name: '已联系', value: 12, width: '56%' },
{ name: '已演示', value: 8, width: '42%' },
{ name: '待支付', value: 7, width: '38%' }
]
const tasks = [
{ title: '增长版线索优先回访', desc: '价格页提交,备注包含“本周想看演示”。', level: 'high' },
{ title: '确认 3 个待支付订单', desc: '需要运营确认线下收款或支付渠道。', level: 'medium' },
{ title: '检查菜单权限配置', desc: '运营角色应隐藏系统管理入口。', level: 'low' }
]
</script>

View File

@@ -0,0 +1,25 @@
<template>
<div class="admin-cms-page">
<section class="admin-page-hero"><div><span class="admin-eyebrow">RECRUIT</span><h2>招聘管理</h2><p>管理加入我们页面展示的职位要求负责人和上下架状态</p></div><button class="primary-button admin-primary-action" type="button" @click="openForm()">新增职位</button></section>
<AdminTablePage eyebrow="JOBS" title="职位列表" description="职位保存后可被加入我们页面读取展示。" :columns="columns" :rows="rows" :loading="loading" :status-options="statusOptions" @refresh="load" @edit="openForm" />
<section class="admin-edit-panel"><div><span class="admin-eyebrow">EDITOR</span><h3>{{ form.id ? '编辑职位' : '新增职位' }}</h3></div><form @submit.prevent="save">
<label><span>职位名称</span><input v-model="form.title" class="form-input" /></label><label><span>部门</span><input v-model="form.department" class="form-input" /></label><label><span>职位类型</span><input v-model="form.job_type" class="form-input" /></label><label><span>城市</span><input v-model="form.city" class="form-input" /></label><label><span>薪资</span><input v-model="form.salary" class="form-input" /></label><label><span>经验</span><input v-model="form.experience" class="form-input" /></label>
<label class="full"><span>职位描述</span><textarea v-model="form.description" class="form-input" rows="3"></textarea></label><label class="full"><span>岗位要求(一行一个)</span><textarea v-model="requirementsText" class="form-input" rows="5"></textarea></label>
<label><span>负责人</span><input v-model="form.manager" class="form-input" /></label><label><span>负责人职位</span><input v-model="form.manager_title" class="form-input" /></label><label><span>邮箱</span><input v-model="form.email" class="form-input" type="email" /></label><label><span>电话</span><input v-model="form.phone" class="form-input" type="tel" /></label><label><span>状态</span><select v-model.number="form.status" class="form-input"><option :value="1">上架</option><option :value="0">下架</option></select></label><label><span>重点招聘</span><select v-model="form.featured" class="form-input"><option :value="true">是</option><option :value="false"></option></select></label>
<button class="primary-button" type="submit">保存职位</button>
</form></section>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import AdminTablePage from './AdminTablePage.vue'
import { getAdminRecruitJobs, saveAdminRecruitJob } from '@/api/request/site'
const columns = [{ key: 'title', label: '职位名称' }, { key: 'department', label: '部门' }, { key: 'job_type', label: '类型', type: 'tag' }, { key: 'city', label: '城市' }, { key: 'salary', label: '薪资' }, { key: 'featured', label: '重点', type: 'bool' }, { key: 'status', label: '状态', type: 'tag', map: { 1: '上架', 0: '下架' } }]
const statusOptions = [{ label: '上架', value: '1' }, { label: '下架', value: '0' }]
const rows = ref([]), loading = ref(false), requirementsText = ref('')
const form = reactive({ id: 0, title: '', department: '', job_type: '全职', city: '杭州', salary: '', experience: '', description: '', requirements: [], manager: '', manager_title: '', email: '', phone: '', featured: false, status: 1, sort: 0 })
const openForm = (row = {}) => { Object.assign(form, { id: 0, title: '', department: '', job_type: '全职', city: '杭州', salary: '', experience: '', description: '', requirements: [], manager: '', manager_title: '', email: '', phone: '', featured: false, status: 1, sort: 0 }, row); requirementsText.value = (form.requirements || []).join('\n') }
const load = async () => { loading.value = true; rows.value = await getAdminRecruitJobs().catch(() => []); loading.value = false }
const save = async () => { form.requirements = requirementsText.value.split('\n').map(item => item.trim()).filter(Boolean); const saved = await saveAdminRecruitJob({ ...form }); openForm(saved); await load() }
onMounted(load)
</script>

View File

@@ -0,0 +1,36 @@
<template>
<AdminTablePage
eyebrow="SITE LEADS"
title="咨询线索"
description="跟进官网预约演示、价格页咨询和套餐意向,优先处理高价值线索。"
primary-action="新增线索"
:columns="columns"
:rows="rows"
:loading="loading"
:status-options="statusOptions"
@refresh="load"
/>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import AdminTablePage from './AdminTablePage.vue'
import { getAdminLeads } from '@/api/request/site'
const columns = [
{ key: 'name', label: '联系人' },
{ key: 'phone', label: '手机号' },
{ key: 'clinic_name', label: '诊所名称' },
{ key: 'city', label: '城市' },
{ key: 'plan_code', label: '意向套餐', type: 'tag', map: { starter: '基础版', growth: '增长版', enterprise: '连锁版' } },
{ key: 'source_page', label: '来源页面', type: 'tag' },
{ key: 'remark', label: '备注' }
]
const statusOptions = [{ label: '增长版', value: 'growth' }, { label: '基础版', value: 'starter' }, { label: '连锁版', value: 'enterprise' }]
const rows = ref([])
const loading = ref(false)
const load = async () => {
loading.value = true
rows.value = await getAdminLeads().catch(() => [])
loading.value = false
}
onMounted(load)
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div class="admin-login">
<form class="admin-login__panel" @submit.prevent="submit">
<span class="eyebrow">ADMIN</span>
<h1>萧康云医后台</h1>
<p>管理官网线索套餐订单角色权限和菜单布局</p>
<input v-model="form.account" class="form-input" placeholder="账号" />
<input v-model="form.password" class="form-input" type="password" placeholder="密码" />
<button class="primary-button justify-center" type="submit">登录后台</button>
</form>
</div>
</template>
<script setup>
import { reactive } from 'vue'
import { useRouter } from 'vue-router'
import { adminLogin } from '@/api/request/admin'
const router = useRouter()
const form = reactive({ account: 'admin', password: '123456' })
const submit = async () => {
const res = await adminLogin(form)
localStorage.setItem('token', res.token)
localStorage.setItem('user', JSON.stringify(res.user))
router.push('/admin/dashboard')
}
</script>

View File

@@ -0,0 +1,23 @@
<template>
<div class="admin-cms-page">
<section class="admin-page-hero"><div><span class="admin-eyebrow">SITE MEDIA</span><h2>图片管理</h2><p>管理 LOGOfavicon/ico页面图片分享图和招聘图真实上传后续接对象存储本期支持 URL 录入</p></div><button class="primary-button admin-primary-action" type="button" @click="openForm()">新增素材</button></section>
<AdminTablePage eyebrow="MEDIA" title="素材列表" description="素材可按类型筛选,并在前台/SEO中引用。" :columns="columns" :rows="rows" :loading="loading" :status-options="statusOptions" @refresh="load" @edit="openForm" />
<section class="admin-edit-panel"><div><span class="admin-eyebrow">EDITOR</span><h3>{{ form.id ? '编辑素材' : '新增素材' }}</h3></div><form @submit.prevent="save">
<label><span>素材类型</span><select v-model="form.media_type" class="form-input"><option value="logo">LOGO</option><option value="favicon">ICO/Favicon</option><option value="page_image">页面图片</option><option value="share_image">分享图</option><option value="job_image">招聘图</option></select></label>
<label><span>分组</span><input v-model="form.group_code" class="form-input" /></label><label><span>标题</span><input v-model="form.title" class="form-input" /></label><label><span>图片 URL</span><input v-model="form.url" class="form-input" /></label><label><span>Alt 文本</span><input v-model="form.alt" class="form-input" /></label><label><span>排序</span><input v-model.number="form.sort" class="form-input" type="number" /></label><button class="primary-button" type="submit">保存素材</button>
</form><div v-if="form.url" class="media-preview"><img :src="form.url" :alt="form.alt" /></div></section>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import AdminTablePage from './AdminTablePage.vue'
import { getAdminSiteMedia, saveAdminSiteMedia } from '@/api/request/site'
const columns = [{ key: 'media_type', label: '类型', type: 'tag' }, { key: 'group_code', label: '分组' }, { key: 'title', label: '标题' }, { key: 'url', label: 'URL' }, { key: 'alt', label: 'Alt' }, { key: 'status', label: '状态', type: 'tag', map: { 1: '启用', 0: '禁用' } }]
const statusOptions = [{ label: 'LOGO', value: 'logo' }, { label: 'ICO', value: 'favicon' }, { label: '分享图', value: 'share_image' }]
const rows = ref([]), loading = ref(false)
const form = reactive({ id: 0, media_type: 'logo', group_code: 'brand', title: '', url: '', alt: '', status: 1, sort: 0 })
const openForm = (row = {}) => Object.assign(form, { id: 0, media_type: 'logo', group_code: 'brand', title: '', url: '', alt: '', status: 1, sort: 0 }, row)
const load = async () => { loading.value = true; rows.value = await getAdminSiteMedia().catch(() => []); loading.value = false }
const save = async () => { const saved = await saveAdminSiteMedia({ ...form }); openForm(saved); await load() }
onMounted(load)
</script>

View File

@@ -0,0 +1,36 @@
<template>
<AdminTablePage
eyebrow="SYSTEM MENUS"
title="菜单管理"
description="查看数据库驱动的后台多级菜单,确认路径、权限标识和双列菜单区域。"
primary-action="新增菜单"
:columns="columns"
:rows="flatRows"
:loading="loading"
:status-options="statusOptions"
@refresh="load"
/>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import AdminTablePage from './AdminTablePage.vue'
import { getAdminMenus } from '@/api/request/admin'
const columns = [
{ key: 'id', label: 'ID' },
{ key: 'title', label: '菜单标题' },
{ key: 'path', label: '路由路径' },
{ key: 'component', label: '组件' },
{ key: 'permission', label: '权限标识', type: 'tag' },
{ key: 'menu_area', label: '菜单区域', type: 'tag', map: { 1: '主列', 2: '次列' } }
]
const statusOptions = [{ label: '主列', value: '1' }, { label: '次列', value: '2' }]
const menus = ref([])
const loading = ref(false)
const flatRows = computed(() => menus.value.flatMap(item => [{ ...item, menu_area: String(item.menu_area) }, ...(item.children || []).map(child => ({ ...child, menu_area: String(child.menu_area) }))]))
const load = async () => {
loading.value = true
menus.value = await getAdminMenus().catch(() => [])
loading.value = false
}
onMounted(load)
</script>

View File

@@ -0,0 +1,37 @@
<template>
<AdminTablePage
eyebrow="COMMERCE ORDERS"
title="订单管理"
description="查看套餐订购订单、支付周期和待处理状态,方便运营快速跟进。"
primary-action="创建订单"
:columns="columns"
:rows="rows"
:loading="loading"
:status-options="statusOptions"
@refresh="load"
/>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import AdminTablePage from './AdminTablePage.vue'
import { getAdminOrders } from '@/api/request/orders'
const columns = [
{ key: 'order_no', label: '订单号' },
{ key: 'tenant_name', label: '诊所名称' },
{ key: 'contact_name', label: '联系人' },
{ key: 'phone', label: '手机号' },
{ key: 'plan_code', label: '套餐', type: 'tag', map: { starter: '基础版', growth: '增长版', enterprise: '连锁版' } },
{ key: 'billing_cycle', label: '周期', type: 'cycle' },
{ key: 'amount', label: '金额', type: 'money' },
{ key: 'status', label: '状态', type: 'tag', map: { pending: '待支付', paid: '已支付', closed: '已关闭' } }
]
const statusOptions = [{ label: '待支付', value: 'pending' }, { label: '已支付', value: 'paid' }, { label: '已关闭', value: 'closed' }]
const rows = ref([])
const loading = ref(false)
const load = async () => {
loading.value = true
rows.value = await getAdminOrders().catch(() => [])
loading.value = false
}
onMounted(load)
</script>

View File

@@ -0,0 +1,35 @@
<template>
<AdminTablePage
eyebrow="SAAS PLANS"
title="套餐管理"
description="维护官网展示套餐、月付年付价格和权益配置,后续可接入完整 CRUD。"
primary-action="新增套餐"
:columns="columns"
:rows="rows"
:loading="loading"
:status-options="statusOptions"
@refresh="load"
/>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import AdminTablePage from './AdminTablePage.vue'
import { getAdminPlans } from '@/api/request/pricing'
const columns = [
{ key: 'name', label: '套餐名称' },
{ key: 'code', label: '套餐编码', type: 'tag' },
{ key: 'description', label: '说明' },
{ key: 'monthly_fee', label: '月付', type: 'money' },
{ key: 'yearly_fee', label: '年付', type: 'money' },
{ key: 'recommended', label: '推荐', type: 'bool' }
]
const statusOptions = [{ label: '推荐套餐', value: 'true' }, { label: '普通套餐', value: 'false' }]
const rows = ref([])
const loading = ref(false)
const load = async () => {
loading.value = true
rows.value = await getAdminPlans().catch(() => [])
loading.value = false
}
onMounted(load)
</script>

View File

@@ -0,0 +1,40 @@
<template>
<div class="admin-settings-page">
<section class="admin-page-hero">
<div>
<span class="admin-eyebrow">SETTINGS</span>
<h2>个人设置</h2>
<p>管理账号信息后台菜单布局和接口规范提示这里先做配置壳后续接入 nl_admin_preference</p>
</div>
<button class="primary-button admin-primary-action" type="button">保存设置</button>
</section>
<div class="admin-settings-grid">
<article v-for="card in cards" :key="card.title" class="admin-setting-card">
<i :class="card.icon"></i>
<h3>{{ card.title }}</h3>
<p>{{ card.desc }}</p>
<button class="admin-button" type="button">{{ card.action }}</button>
</article>
</div>
<section class="admin-board admin-board-v2">
<div class="admin-board__head">
<div><span class="admin-eyebrow">API RULES</span><h3>接口规则</h3></div>
</div>
<ul class="admin-rule-list">
<li>HTTP 状态码统一 200业务状态由 code 区分</li>
<li>响应固定为 codemessageresult</li>
<li>JSON 字段统一 snake_case例如 order_nomenu_layout</li>
</ul>
</section>
</div>
</template>
<script setup>
const cards = [
{ icon: 'fas fa-table-columns', title: '菜单布局', desc: '支持单列与双列,个人偏好优先于全局配置。', action: '切换布局' },
{ icon: 'fas fa-user-shield', title: '账号安全', desc: '后续接入操作密码、登录 IP 和密码更新时间。', action: '查看安全' },
{ icon: 'fas fa-moon', title: '后台主题', desc: '跟随官网灰黑暗色体系,保证夜间值班可读。', action: '主题设置' }
]
</script>

View File

@@ -0,0 +1,30 @@
<template>
<AdminTablePage
eyebrow="SYSTEM ROLES"
title="角色权限"
description="管理后台角色和授权关系,后续可在详情抽屉内配置菜单权限。"
primary-action="新增角色"
:columns="columns"
:rows="rows"
:loading="loading"
@refresh="load"
/>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import AdminTablePage from './AdminTablePage.vue'
import { getRoles } from '@/api/request/admin'
const columns = [
{ key: 'id', label: 'ID' },
{ key: 'name', label: '角色名称' },
{ key: 'code', label: '角色编码', type: 'tag' }
]
const rows = ref([])
const loading = ref(false)
const load = async () => {
loading.value = true
rows.value = await getRoles().catch(() => [])
loading.value = false
}
onMounted(load)
</script>

View File

@@ -0,0 +1,37 @@
<template>
<div class="admin-cms-page">
<section class="admin-page-hero">
<div><span class="admin-eyebrow">SITE SECTIONS</span><h2>页面模块</h2><p>控制每个页面每块内容的标题副标题JSON 内容排序和启用状态</p></div>
<button class="primary-button admin-primary-action" type="button" @click="openForm()">新增模块</button>
</section>
<AdminTablePage eyebrow="CMS" title="页面模块列表" description="模块内容以 JSON 承载,后续可接入可视化编辑器。" :columns="columns" :rows="rows" :loading="loading" :status-options="statusOptions" @refresh="load" @edit="openForm" @primary="openForm()" />
<section class="admin-edit-panel">
<div><span class="admin-eyebrow">EDITOR</span><h3>{{ form.id ? '编辑模块' : '新增模块' }}</h3></div>
<form @submit.prevent="save">
<label><span>页面编码</span><input v-model="form.page_code" class="form-input" /></label>
<label><span>模块标识</span><input v-model="form.section_key" class="form-input" /></label>
<label><span>标题</span><input v-model="form.title" class="form-input" /></label>
<label><span>副标题</span><input v-model="form.subtitle" class="form-input" /></label>
<label><span>排序</span><input v-model.number="form.sort" class="form-input" type="number" /></label>
<label><span>状态</span><select v-model.number="form.status" class="form-input"><option :value="1">启用</option><option :value="0">禁用</option></select></label>
<label class="full"><span>内容 JSON</span><textarea v-model="contentJson" class="form-input" rows="6"></textarea></label>
<button class="primary-button" type="submit">保存模块</button>
</form>
</section>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import AdminTablePage from './AdminTablePage.vue'
import { getAdminSiteSections, saveAdminSiteSection } from '@/api/request/site'
const columns = [{ key: 'page_code', label: '页面' }, { key: 'section_key', label: '模块标识', type: 'tag' }, { key: 'title', label: '标题' }, { key: 'subtitle', label: '副标题' }, { key: 'status', label: '状态', type: 'tag', map: { 1: '启用', 0: '禁用' } }, { key: 'sort', label: '排序' }]
const statusOptions = [{ label: '启用', value: '1' }, { label: '禁用', value: '0' }]
const rows = ref([])
const loading = ref(false)
const contentJson = ref('{\n "layout": "editorial"\n}')
const form = reactive({ id: 0, page_code: 'home', section_key: '', title: '', subtitle: '', content: {}, status: 1, sort: 0 })
const openForm = (row = {}) => { Object.assign(form, { id: 0, page_code: 'home', section_key: '', title: '', subtitle: '', content: {}, status: 1, sort: 0 }, row); contentJson.value = JSON.stringify(form.content || {}, null, 2) }
const load = async () => { loading.value = true; rows.value = await getAdminSiteSections().catch(() => []); loading.value = false }
const save = async () => { form.content = JSON.parse(contentJson.value || '{}'); const saved = await saveAdminSiteSection({ ...form }); openForm(saved); await load() }
onMounted(load)
</script>

View File

@@ -0,0 +1,18 @@
<template>
<div class="admin-cms-page">
<section class="admin-page-hero"><div><span class="admin-eyebrow">SITE SETTINGS</span><h2>站点设置</h2><p>控制全站标题LOGOico默认 SEOGSO/GEO联系方式和地图密钥</p></div><button class="primary-button admin-primary-action" type="button" @click="save">保存设置</button></section>
<section class="admin-edit-panel site-settings-panel"><div><span class="admin-eyebrow">BRAND</span><h3>品牌与基础信息</h3></div><form @submit.prevent="save">
<label><span>站点标题</span><input v-model="form.site_title" class="form-input" /></label><label><span>站点 LOGO</span><input v-model="form.site_logo" class="form-input" /></label><label><span>暗色 LOGO</span><input v-model="form.site_logo_dark" class="form-input" /></label><label><span>ICO/Favicon</span><input v-model="form.site_favicon" class="form-input" /></label><label><span>联系电话</span><input v-model="form.contact_phone" class="form-input" /></label><label><span>联系邮箱</span><input v-model="form.contact_email" class="form-input" /></label><label class="full"><span>联系地址</span><input v-model="form.contact_address" class="form-input" /></label><label><span>备案信息</span><input v-model="form.icp_text" class="form-input" /></label><label><span>版权信息</span><input v-model="form.copyright" class="form-input" /></label>
<label class="full"><span>默认 SEO JSON</span><textarea v-model="seoJson" class="form-input" rows="5"></textarea></label><label class="full"><span>默认 GSO/GEO JSON</span><textarea v-model="gsoJson" class="form-input" rows="5"></textarea></label><label><span>高德 Key</span><input v-model="form.amap_key" class="form-input" /></label><label><span>高德安全密钥</span><input v-model="form.amap_security_code" class="form-input" /></label><button class="primary-button" type="submit">保存设置</button>
</form><div v-if="message" class="pricing-message">{{ message }}</div></section>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { getAdminSiteSettings, saveAdminSiteSettings } from '@/api/request/site'
const message = ref(''), seoJson = ref('{}'), gsoJson = ref('{}')
const form = reactive({ site_title: '', site_logo: '', site_logo_dark: '', site_favicon: '', icp_text: '', copyright: '', contact_phone: '', contact_email: '', contact_address: '', default_seo: {}, default_gso: {}, amap_key: '', amap_security_code: '' })
const load = async () => { const data = await getAdminSiteSettings().catch(() => ({})); Object.assign(form, data); seoJson.value = JSON.stringify(form.default_seo || {}, null, 2); gsoJson.value = JSON.stringify(form.default_gso || {}, null, 2) }
const save = async () => { form.default_seo = JSON.parse(seoJson.value || '{}'); form.default_gso = JSON.parse(gsoJson.value || '{}'); await saveAdminSiteSettings({ ...form }); message.value = '站点设置已保存。' }
onMounted(load)
</script>

View File

@@ -0,0 +1,151 @@
<template>
<div class="admin-data-page">
<div class="admin-page-hero">
<div>
<span class="admin-eyebrow">{{ eyebrow }}</span>
<h2>{{ title }}</h2>
<p>{{ description }}</p>
</div>
<div class="admin-page-actions">
<button class="admin-button" type="button" @click="$emit('refresh')">
<i class="fas fa-rotate"></i>
刷新
</button>
<button v-if="primaryAction" class="primary-button admin-primary-action" type="button" @click="$emit('primary')">
<i :class="primaryIcon"></i>
{{ primaryAction }}
</button>
</div>
</div>
<div class="admin-filter-bar">
<label class="admin-search-field">
<i class="fas fa-search"></i>
<input v-model="keyword" :placeholder="`搜索${title}`" />
</label>
<select v-model="statusFilter" class="admin-select">
<option value="">全部状态</option>
<option v-for="option in statusOptions" :key="option.value" :value="option.value">{{ option.label }}</option>
</select>
<button class="admin-button" type="button" @click="clearFilters">清空筛选</button>
<div class="admin-filter-meta">{{ filteredRows.length }} 条记录</div>
</div>
<div class="admin-table-card">
<div v-if="loading" class="admin-loading-state">
<i></i><i></i><i></i><i></i>
</div>
<div v-else-if="!filteredRows.length" class="admin-empty-state">
<i class="fas fa-inbox"></i>
<strong>暂无数据</strong>
<span>可以调整筛选条件或刷新接口后再看</span>
</div>
<table v-else>
<thead>
<tr>
<th v-for="col in normalizedColumns" :key="col.key">{{ col.label }}</th>
<th class="admin-actions-col">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in pagedRows" :key="rowKey(row)" @click="selectRow(row)">
<td v-for="col in normalizedColumns" :key="col.key">
<component :is="cellComponent(col)" :value="row[col.key]" :column="col" />
</td>
<td class="admin-row-actions" @click.stop>
<button type="button" @click="selectRow(row)">详情</button>
<button type="button" @click="emit('edit', row)">编辑</button>
<button type="button">跟进</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="admin-pagination" v-if="filteredRows.length">
<span> {{ page }} / {{ totalPages }} </span>
<div>
<button class="admin-button" type="button" :disabled="page === 1" @click="page--">上一页</button>
<button class="admin-button" type="button" :disabled="page === totalPages" @click="page++">下一页</button>
</div>
</div>
<transition name="admin-drawer">
<aside v-if="selectedRow" class="admin-detail-drawer">
<div class="admin-detail-drawer__head">
<div>
<span class="admin-eyebrow">DETAIL</span>
<h3>{{ title }}详情</h3>
</div>
<button class="admin-icon-btn" type="button" @click="selectedRow = null"><i class="fas fa-times"></i></button>
</div>
<dl>
<template v-for="col in normalizedColumns" :key="col.key">
<dt>{{ col.label }}</dt>
<dd><component :is="cellComponent(col)" :value="selectedRow[col.key]" :column="col" /></dd>
</template>
</dl>
</aside>
</transition>
</div>
</template>
<script setup>
import { computed, h, ref, watch } from 'vue'
const props = defineProps({
eyebrow: { type: String, default: 'ADMIN' },
title: { type: String, default: '数据列表' },
description: { type: String, default: '查看、筛选并处理后台业务数据。' },
columns: { type: Array, default: () => [] },
rows: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
primaryAction: { type: String, default: '' },
primaryIcon: { type: String, default: 'fas fa-plus' },
statusOptions: { type: Array, default: () => [] },
pageSize: { type: Number, default: 8 }
})
const emit = defineEmits(['refresh', 'primary', 'edit'])
const keyword = ref('')
const statusFilter = ref('')
const page = ref(1)
const selectedRow = ref(null)
const normalizedColumns = computed(() => props.columns.map((col) => typeof col === 'string' ? { key: col, label: col, type: 'text' } : { type: 'text', ...col }))
const searchableKeys = computed(() => normalizedColumns.value.map(col => col.key))
const filteredRows = computed(() => {
const text = keyword.value.trim().toLowerCase()
return props.rows.filter((row) => {
const matchText = !text || searchableKeys.value.some(key => String(row[key] ?? '').toLowerCase().includes(text))
const matchStatus = !statusFilter.value || String(row.status ?? row.follow_status ?? row.pay_status ?? row.recommended ?? '') === statusFilter.value
return matchText && matchStatus
})
})
const totalPages = computed(() => Math.max(1, Math.ceil(filteredRows.value.length / props.pageSize)))
const pagedRows = computed(() => filteredRows.value.slice((page.value - 1) * props.pageSize, page.value * props.pageSize))
watch(filteredRows, () => { page.value = 1 })
const clearFilters = () => {
keyword.value = ''
statusFilter.value = ''
}
const selectRow = (row) => { selectedRow.value = row }
const rowKey = (row) => row.id || row.order_no || row.code || row.phone || JSON.stringify(row)
const formatMoney = (value) => value === undefined || value === null || value === '-' ? '-' : `¥${Number(value / 100).toLocaleString('zh-CN')}`
const formatCycle = (value) => value === 'year' ? '年付' : value === 'month' ? '月付' : value || '-'
const formatBool = (value) => Number(value) === 1 || value === true ? '是' : '否'
const textCell = (props) => h('span', { class: 'admin-cell-text' }, String(props.value ?? '-'))
const moneyCell = (props) => h('strong', { class: 'admin-cell-money' }, formatMoney(props.value))
const tagCell = (props) => h('span', { class: `admin-status-badge is-${props.value || 'empty'}` }, props.column.map?.[props.value] || props.value || '-')
const cycleCell = (props) => h('span', { class: 'admin-soft-badge' }, formatCycle(props.value))
const boolCell = (props) => h('span', { class: `admin-status-badge ${props.value ? 'is-success' : 'is-empty'}` }, formatBool(props.value))
const cellComponent = (col) => {
const cells = { money: moneyCell, tag: tagCell, cycle: cycleCell, bool: boolCell }
return cells[col.type] || textCell
}
</script>

View File

@@ -19,7 +19,7 @@ export default defineConfig({
proxy: { proxy: {
'/api': { '/api': {
// target: 'http://127.0.0.1:18007/api/', // target: 'http://127.0.0.1:18007/api/',
target: 'http://127.0.0.1:18001/api/', target: 'http://127.0.0.1:8000/api/',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '') rewrite: (path) => path.replace(/^\/api/, '')
} }