初始化
This commit is contained in:
12
.cursor/rules/00-global.mdc
Normal file
12
.cursor/rules/00-global.mdc
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
description: 萧康云医项目全局编码规则
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 全局规则
|
||||
|
||||
- 代码、包、方法、结构体、接口、关键业务分支、复杂判断、关键配置均需要编写清晰的中文注释。
|
||||
- 注释要说明业务意图、输入输出、边界条件和注意事项,避免只重复代码字面含义。
|
||||
- 模块职责要清晰,避免跨层调用、重复实现和无关逻辑混入。
|
||||
- 新增或修改功能时,优先遵循项目已有目录结构、命名方式、封装习惯和错误处理方式。
|
||||
- 公共能力应沉淀为可复用模块或组件,避免在多个页面、控制器或服务中复制粘贴。
|
||||
17
.cursor/rules/10-frontend.mdc
Normal file
17
.cursor/rules/10-frontend.mdc
Normal 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 组件组成。
|
||||
- 组件应保持职责单一,状态、事件、属性和插槽设计清楚,避免单个组件承担过多页面逻辑。
|
||||
- 表单、列表、筛选、弹窗、空状态、加载态、错误态、成功态等常见状态要完整设计。
|
||||
- 视觉风格要克制、现代、可信,适合医疗和诊所经营场景;避免过度营销化、装饰化或单一色系堆叠。
|
||||
17
.cursor/rules/20-backend-goframe.mdc
Normal file
17
.cursor/rules/20-backend-goframe.mdc
Normal 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
1
.gitignore
vendored
@@ -22,3 +22,4 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
/.cursor/skills/
|
||||
|
||||
33
AGENTS.md
Normal file
33
AGENTS.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# 项目协作规则
|
||||
|
||||
## 全局规则
|
||||
|
||||
- 代码、包、方法、结构体、接口、关键业务分支、复杂判断、关键配置均需要编写清晰的中文注释。
|
||||
- 注释要说明业务意图、输入输出、边界条件和注意事项,避免只重复代码字面含义。
|
||||
- 模块职责要清晰,避免跨层调用、重复实现和无关逻辑混入。
|
||||
- 新增或修改功能时,优先遵循项目已有目录结构、命名方式、封装习惯和错误处理方式。
|
||||
- 公共能力应沉淀为可复用模块或组件,避免在多个页面、控制器或服务中复制粘贴。
|
||||
|
||||
## 前端规则
|
||||
|
||||
- 项目名称为“萧康云医官网”,定位为面向诊所赋能业务的 SaaS 平台。
|
||||
- 产品端形态包含小程序患者端、医生端、门店老板端、推广员端,以及 PC 端管理或官网相关页面。
|
||||
- 页面设计要体现 SaaS 平台的专业、清晰、高效,优先服务信息扫描、业务转化和多角色理解。
|
||||
- 必须注意多端适配,兼容移动端、平板端和桌面端,不允许出现文字溢出、布局重叠、关键按钮不可触达等问题。
|
||||
- 必须提供暗色模式适配,颜色、阴影、边框、图标、图表和状态提示都要在明暗主题下保持可读。
|
||||
- 采用模块化、组件化设计,页面由清晰的业务模块和可复用 UI 组件组成。
|
||||
- 组件应保持职责单一,状态、事件、属性和插槽设计清楚,避免单个组件承担过多页面逻辑。
|
||||
- 表单、列表、筛选、弹窗、空状态、加载态、错误态、成功态等常见状态要完整设计。
|
||||
- 视觉风格要克制、现代、可信,适合医疗和诊所经营场景;避免过度营销化、装饰化或单一色系堆叠。
|
||||
|
||||
## 后端规则
|
||||
|
||||
- 后端使用 GoFrame GF 框架,必须遵循 GF 的项目结构、路由注册、控制器、服务、模型、配置和错误处理规范。
|
||||
- 分层链路为:路由 -> 控制器 -> service 服务层 -> 模型层。
|
||||
- 路由层只负责请求路径、HTTP 方法、中间件和控制器绑定,不编写业务逻辑。
|
||||
- 控制器层负责获取请求信息、参数验证、用户信息解析、调用 service,并返回统一响应。
|
||||
- 控制器层不编写核心业务逻辑,不直接操作数据库,不绕过 service 调用模型。
|
||||
- service 层是业务逻辑层,负责业务流程编排、权限判断、事务控制、数据处理和外部服务调用。
|
||||
- 模型层只用于定义表结构、表关系、字段映射和数据承载,不编写业务逻辑。
|
||||
- 数据库访问、事务、缓存、队列、第三方接口等能力应按 GF 推荐方式封装,并放在合适的服务或基础设施模块中。
|
||||
- 参数校验、错误码、日志、上下文传递和用户身份解析要保持统一规范。
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- 在public/index.html的head中添加 -->
|
||||
<!-- <script src="https://webapi.amap.com/maps?v=2.0&key=您的高德地图API密钥"></script>-->
|
||||
<title>Vite + Vue</title>
|
||||
<title>萧康云医官网</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
5
pnpm-workspace.yaml
Normal file
5
pnpm-workspace.yaml
Normal 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
16
src/api/request/admin.js
Normal 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')
|
||||
7
src/api/request/orders.js
Normal file
7
src/api/request/orders.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { get, post } from '@/api/request'
|
||||
|
||||
// 创建套餐订购订单。
|
||||
export const createOrder = (data) => post('/orders', data)
|
||||
|
||||
// 获取后台订单列表。
|
||||
export const getAdminOrders = () => get('/admin/orders')
|
||||
7
src/api/request/pricing.js
Normal file
7
src/api/request/pricing.js
Normal 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
46
src/api/request/site.js
Normal 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
53
src/components/admin/AdminLayout.vue
Normal file
53
src/components/admin/AdminLayout.vue
Normal 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>
|
||||
77
src/components/admin/AdminSidebar.vue
Normal file
77
src/components/admin/AdminSidebar.vue
Normal 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>
|
||||
43
src/components/admin/AdminTopbar.vue
Normal file
43
src/components/admin/AdminTopbar.vue
Normal 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>
|
||||
28
src/components/home/ClinicJourney.vue
Normal file
28
src/components/home/ClinicJourney.vue
Normal 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>
|
||||
42
src/components/home/DataCurrent.vue
Normal file
42
src/components/home/DataCurrent.vue
Normal 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>
|
||||
53
src/components/home/HeroCinematic.vue
Normal file
53
src/components/home/HeroCinematic.vue
Normal 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>
|
||||
38
src/components/home/RoleConstellation.vue
Normal file
38
src/components/home/RoleConstellation.vue
Normal 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>
|
||||
32
src/components/home/ScenarioShowcase.vue
Normal file
32
src/components/home/ScenarioShowcase.vue
Normal 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>
|
||||
@@ -1,101 +1,46 @@
|
||||
<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="flex flex-wrap -mx-4">
|
||||
<!-- 公司信息部分 -->
|
||||
<div class="w-full md:w-1/3 px-4 mb-8 md:mb-0">
|
||||
<div>
|
||||
<router-link to="/" class="flex items-center mb-6">
|
||||
<img
|
||||
src="@/assets/images/logo-white.png"
|
||||
alt="酉佰"
|
||||
class="h-8 md:h-10"
|
||||
/>
|
||||
<span class="ml-2 text-xl md:text-2xl font-bold text-white"
|
||||
>酉佰</span
|
||||
>
|
||||
</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 class="grid gap-10 md:grid-cols-[1.3fr_2fr]">
|
||||
<div>
|
||||
<router-link to="/" class="mb-5 flex items-center">
|
||||
<img src="@/assets/images/logo-white.png" alt="萧康云医" class="h-10" />
|
||||
<span class="ml-3 text-2xl font-bold">萧康云医</span>
|
||||
</router-link>
|
||||
<p class="max-w-sm text-sm leading-7 text-slate-400">
|
||||
萧康云医官网面向诊所经营者、医生团队和推广团队,提供患者端、医生端、门店老板端、推广员端与 PC 后台一体化 SaaS 能力。
|
||||
</p>
|
||||
<div class="mt-6 flex gap-3">
|
||||
<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="社交媒体链接">
|
||||
<i :class="item.icon"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航链接部分 -->
|
||||
<div
|
||||
v-for="(block, index) in footerLinks"
|
||||
:key="index"
|
||||
class="w-full sm:w-1/2 md:w-1/4 px-4 mb-8 md:mb-0"
|
||||
>
|
||||
<h3 class="text-base sm:text-lg font-semibold mb-4">{{ block.title }}</h3>
|
||||
<ul class="space-y-2">
|
||||
<li v-for="(link, linkIndex) in block.links" :key="linkIndex">
|
||||
<a
|
||||
:href="link.path"
|
||||
class="text-gray-400 hover:text-primary transition-colors text-sm sm:text-base"
|
||||
>
|
||||
{{ link.title }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="grid gap-8 sm:grid-cols-3">
|
||||
<div v-for="block in footerLinks" :key="block.title">
|
||||
<h3 class="mb-4 text-base font-semibold">{{ block.title }}</h3>
|
||||
<ul class="space-y-3">
|
||||
<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">
|
||||
{{ link.title }}
|
||||
</router-link>
|
||||
<a v-else :href="link.path" class="text-sm text-slate-400 transition hover:text-sky-300">
|
||||
{{ link.title }}
|
||||
</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 class="border-t border-gray-800 mt-10 pt-6">
|
||||
<div class="flex flex-col md:flex-row justify-between items-center text-center md:text-left">
|
||||
<div class="mb-4 md:mb-0">
|
||||
<p class="text-gray-500 text-xs sm:text-sm">
|
||||
© 2021 shyoubai.com. All rights reserved
|
||||
</p>
|
||||
</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 class="mt-10 border-t border-white/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">
|
||||
<p>© 2026 萧康云医官网. All rights reserved.</p>
|
||||
<div class="flex flex-col items-center gap-3 sm:flex-row">
|
||||
<a href="https://beian.miit.gov.cn/" class="hover:text-sky-300">沪ICP备2021007173号-1</a>
|
||||
<span class="hidden sm:inline">|</span>
|
||||
<a href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=31010402009014" class="hover:text-sky-300">沪公网安备 31010402009014号</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,26 +56,31 @@ export default {
|
||||
social: [
|
||||
{ icon: 'fab fa-weixin', link: '#' },
|
||||
{ icon: 'fab fa-weibo', link: '#' },
|
||||
{ icon: 'fab fa-linkedin-in', link: '#' },
|
||||
{ icon: 'fab fa-zhihu', link: '#' },
|
||||
{ icon: 'fab fa-linkedin-in', link: '#' }
|
||||
],
|
||||
footerLinks: [
|
||||
{
|
||||
title: '快速导航',
|
||||
title: '产品',
|
||||
links: [
|
||||
{ title: '关于我们', path: '/about' },
|
||||
{ title: '团队介绍', path: '/team' },
|
||||
{ title: '服务特色', path: '/features' },
|
||||
{ title: '加入我们', path: '/careers' },
|
||||
{ title: '产品能力', path: '/features' },
|
||||
{ title: '解决方案', path: '/solutions' },
|
||||
{ title: '价格方案', path: '/pricing' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '联系我们',
|
||||
title: '公司',
|
||||
links: [
|
||||
{ title: '关于我们', path: '/about' },
|
||||
{ title: '团队介绍', path: '/team' },
|
||||
{ title: '加入我们', path: '/careers' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '联系',
|
||||
links: [
|
||||
{ title: '上海市中山西路2020号华宜大厦2号楼1501室', path: '#' },
|
||||
{ title: '021-34161621', path: 'tel:021-34161621' },
|
||||
{ title: 'youbaiwangluo@163.com', path: 'mailto:youbaiwangluo@163.com' },
|
||||
{ title: '周一至周五 9:00-18:00', path: '#' },
|
||||
{ title: 'youbaiwangluo@163.com', path: 'mailto:youbaiwangluo@163.com' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,304 +1,43 @@
|
||||
<template>
|
||||
<header
|
||||
class="fixed top-0 w-full z-50 transition-all duration-300"
|
||||
:class="{
|
||||
'bg-white/90 backdrop-blur-md shadow-sm py-3': isScrolled || isMenuOpen,
|
||||
'bg-transparent py-5': !isScrolled && !isMenuOpen
|
||||
}"
|
||||
>
|
||||
<!-- 关键修复:移动端布局优化 -->
|
||||
<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>
|
||||
<template>
|
||||
<header class="fixed inset-x-0 top-0 z-50 border-b border-transparent transition-all duration-300" :class="headerClass">
|
||||
<div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
|
||||
<router-link to="/" class="flex min-w-0 items-center">
|
||||
<img :src="logoSrc" alt="萧康云医" class="h-9 w-9 shrink-0 rounded-lg object-contain" @error="logoSrc = defaultLogo" />
|
||||
<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>
|
||||
|
||||
<!-- 移动端菜单按钮 - 关键修复区域 -->
|
||||
<div class="flex items-center space-x-2 md:hidden">
|
||||
<!-- 移动端语言切换按钮 - 尺寸优化 -->
|
||||
<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>
|
||||
</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="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>
|
||||
</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>
|
||||
|
||||
<!-- 模态框组件 -->
|
||||
<!-- 模态框组件 - 移动端优化 -->
|
||||
<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>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import ContactForm from "@/components/ui/ContactForm.vue";
|
||||
|
||||
const isScrolled = ref(false);
|
||||
const isMenuOpen = ref(false);
|
||||
const showModal = ref(false);
|
||||
const headerHeight = ref(0);
|
||||
const currentLang = ref('zh'); // 默认中文
|
||||
|
||||
// 新增:视口和导航宽度变量
|
||||
const viewportWidth = ref(0);
|
||||
const navWidth = ref(0);
|
||||
|
||||
// 菜单项根据语言动态变化
|
||||
const menuItems = computed(() => {
|
||||
return [
|
||||
{ path: '/', name: currentLang.value === 'zh' ? '首页' : 'Home', exact: true },
|
||||
{ path: '/about', name: currentLang.value === 'zh' ? '公司介绍' : 'About' },
|
||||
{ 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 = ''; // 确保卸载时恢复滚动
|
||||
});
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import ContactForm from '@/components/ui/ContactForm.vue'
|
||||
import { getSiteSettings } from '@/api/request/site'
|
||||
import defaultLogo from '@/assets/images/logo.png'
|
||||
const isScrolled = ref(false), isMenuOpen = ref(false), showModal = ref(false), isDark = ref(false)
|
||||
const brandName = ref('萧康云医')
|
||||
const logoSrc = ref(defaultLogo)
|
||||
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 toggleTheme = () => { isDark.value = !isDark.value; applyTheme() }
|
||||
const openModal = () => { showModal.value = true }
|
||||
const closeModal = () => { showModal.value = false }
|
||||
const toggleMenu = () => { isMenuOpen.value = !isMenuOpen.value; document.body.style.overflow = isMenuOpen.value ? 'hidden' : '' }
|
||||
const closeMenu = () => { isMenuOpen.value = false; document.body.style.overflow = '' }
|
||||
const handleScroll = () => { isScrolled.value = window.scrollY > 10 }
|
||||
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 })
|
||||
onBeforeUnmount(() => { window.removeEventListener('scroll', handleScroll); document.body.style.overflow = '' })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.router-link-exact-active {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
.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; }
|
||||
</style>
|
||||
23
src/components/ui/EditorialCard.vue
Normal file
23
src/components/ui/EditorialCard.vue
Normal 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>
|
||||
22
src/components/ui/FeatureLineCard.vue
Normal file
22
src/components/ui/FeatureLineCard.vue
Normal 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>
|
||||
@@ -1,84 +1,82 @@
|
||||
<!-- src/components/MapContainer.vue -->
|
||||
<template>
|
||||
<div id="map-container" class="w-full h-full"></div>
|
||||
<template>
|
||||
<div class="map-shell">
|
||||
<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>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, defineProps } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import AMapLoader from '@amap/amap-jsapi-loader'
|
||||
|
||||
const props = defineProps({
|
||||
lng: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
lat: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
zoom: {
|
||||
type: Number,
|
||||
default: 16
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '公司位置'
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
lng: { type: Number, required: true },
|
||||
lat: { type: Number, required: true },
|
||||
zoom: { type: Number, default: 16 },
|
||||
title: { type: String, default: '公司位置' },
|
||||
address: { type: String, default: '' },
|
||||
amapKey: { type: String, default: import.meta.env.VITE_AMAP_KEY || '' },
|
||||
securityJsCode: { type: String, default: import.meta.env.VITE_AMAP_SECURITY_CODE || '' }
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 安全密钥配置
|
||||
window._AMapSecurityConfig = {
|
||||
securityJsCode: "test123"
|
||||
}
|
||||
const mapRef = ref(null)
|
||||
const mapInstance = ref(null)
|
||||
const loading = ref(false)
|
||||
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({
|
||||
key: "testkey",
|
||||
version: "2.0",
|
||||
plugins: ["AMap.InfoWindow"]
|
||||
}).then((AMap) => {
|
||||
// 初始化地图
|
||||
const map = new AMap.Map('map-container', {
|
||||
zoom: props.zoom,
|
||||
center: [props.lng, props.lat],
|
||||
viewMode: '3D'
|
||||
})
|
||||
|
||||
// 添加标记
|
||||
const marker = new AMap.Marker({
|
||||
position: [props.lng, props.lat],
|
||||
title: props.title
|
||||
})
|
||||
map.add(marker)
|
||||
|
||||
// 添加信息窗口
|
||||
const initMap = async () => {
|
||||
if (!canLoadMap.value) return
|
||||
loading.value = true
|
||||
hasError.value = false
|
||||
try {
|
||||
window._AMapSecurityConfig = { securityJsCode: props.securityJsCode }
|
||||
const AMap = await AMapLoader.load({ key: props.amapKey, version: '2.0', plugins: ['AMap.InfoWindow'] })
|
||||
await nextTick()
|
||||
if (!mapRef.value) return
|
||||
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 infoWindow = new AMap.InfoWindow({
|
||||
content: `<div class="p-2">
|
||||
<h4 class="font-bold">${props.title}</h4>
|
||||
<p class="text-sm">${props.address}</p>
|
||||
</div>`,
|
||||
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>`,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 默认打开信息窗口
|
||||
infoWindow.open(map, [props.lng, props.lat])
|
||||
|
||||
// 点击标记打开信息窗口
|
||||
marker.on('click', () => {
|
||||
infoWindow.open(map, [props.lng, props.lat])
|
||||
})
|
||||
}).catch(e => {
|
||||
console.error('地图加载失败', e)
|
||||
})
|
||||
onMounted(initMap)
|
||||
onBeforeUnmount(() => {
|
||||
mapInstance.value?.destroy?.()
|
||||
mapInstance.value = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#map-container {
|
||||
min-height: 300px;
|
||||
}
|
||||
.map-shell { position: relative; min-height: 380px; overflow: hidden; border: 1px solid var(--line); border-radius: 14px; background: var(--panel-bg); }
|
||||
.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>
|
||||
|
||||
16
src/components/ui/MetricArtCard.vue
Normal file
16
src/components/ui/MetricArtCard.vue
Normal 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>
|
||||
33
src/components/ui/PricingRowCard.vue
Normal file
33
src/components/ui/PricingRowCard.vue
Normal 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>
|
||||
23
src/components/ui/ScenarioImageCard.vue
Normal file
23
src/components/ui/ScenarioImageCard.vue
Normal 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>
|
||||
@@ -1,13 +1,36 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { getSiteSettings } from '@/api/request/site'
|
||||
|
||||
let cachedSiteSettings = null
|
||||
|
||||
// 在路由配置中添加个人中心路由
|
||||
const routes = [
|
||||
{ path: '/', meta: { title: '首页' }, name: 'Home', component: () => import('@/views/Home.vue') },
|
||||
{ path: '/about', meta: { title: '公司介绍' }, name: 'About', component: () => import('@/views/About.vue') },
|
||||
{ path: '/team', meta: { title: '团队介绍' }, name: 'Team', component: () => import('@/views/Team.vue') },
|
||||
{ path: '/features', meta: { title: '公司特色' }, name: 'Features', component: () => import('@/views/Features.vue') },
|
||||
{ path: '/careers', meta: { title: '加入我们' }, name: 'Careers', component: () => import('@/views/Careers.vue') },
|
||||
{ path: '/', meta: { title: '首页', page_code: 'home' }, name: 'Home', component: () => import('@/views/Home.vue') },
|
||||
{ path: '/features', meta: { title: '产品能力', page_code: 'features' }, name: 'Features', component: () => import('@/views/Features.vue') },
|
||||
{ path: '/solutions', meta: { title: '解决方案', page_code: 'solutions' }, name: 'Solutions', component: () => import('@/views/Solutions.vue') },
|
||||
{ path: '/pricing', meta: { title: '价格方案', page_code: 'pricing' }, name: 'Pricing', component: () => import('@/views/Pricing.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({
|
||||
@@ -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
|
||||
|
||||
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 = `上海酉佰`
|
||||
}
|
||||
})
|
||||
|
||||
@@ -21,12 +21,12 @@
|
||||
<div class="order-2 lg:order-1" data-aos="fade-right" data-aos-duration="800">
|
||||
<SectionTitle
|
||||
subtitle="关于我们"
|
||||
title="我们是上海酉佰"
|
||||
title="我们是萧康云医"
|
||||
/>
|
||||
|
||||
<div class="prose max-w-none text-gray-600 mt-6">
|
||||
<p>
|
||||
上海酉佰有限公司成立于2010年,总部位于上海浦东新区,
|
||||
萧康云医专注诊所赋能与医疗服务数字化,总部位于上海,
|
||||
是一家专注于提供企业级数字化解决方案的高新技术企业。
|
||||
</p>
|
||||
<p>
|
||||
@@ -176,7 +176,7 @@
|
||||
title="发展历程"
|
||||
subtitle="一路成长"
|
||||
align="center"
|
||||
description="回顾上海酉佰发展中的重要里程碑"
|
||||
description="回顾萧康云医发展中的重要里程碑"
|
||||
data-aos="fade-up"
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,719 +1,177 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 页面标题 -->
|
||||
<div
|
||||
class="pt-32 pb-20 bg-gradient-to-r from-indigo-700 to-blue-900"
|
||||
data-aos="fade-down"
|
||||
>
|
||||
<div class="page-shell careers-page-v2">
|
||||
<section class="subpage-hero careers-hero">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">加入我们</h1>
|
||||
<p class="text-xl text-blue-300 max-w-2xl mx-auto">
|
||||
与优秀团队一起成长,创造改变世界的价值
|
||||
</p>
|
||||
<div class="subpage-hero__grid">
|
||||
<div data-aos="fade-up">
|
||||
<span class="subpage-kicker">JOIN XIAOKANG</span>
|
||||
<h1>和我们一起,把诊所经营变得更清楚。</h1>
|
||||
<p>萧康云医正在寻找理解医疗服务、也愿意打磨产品细节的伙伴。这里的工作不只是做页面或做运营,而是把真实诊所的复杂日常整理成可靠系统。</p>
|
||||
</div>
|
||||
<div class="careers-hero__panel" data-aos="fade-left">
|
||||
<strong>{{ filteredJobs.length }}</strong>
|
||||
<span>开放职位</span>
|
||||
<p>技术、产品、运营和诊所服务顾问持续招聘中。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 职位列表 -->
|
||||
<section class="py-16 md:py-24">
|
||||
<section class="editorial-section">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div class="lg:col-span-1">
|
||||
<SectionTitle
|
||||
title="开放职位"
|
||||
subtitle="寻找卓越人才"
|
||||
data-aos="fade-right"
|
||||
/>
|
||||
|
||||
<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 class="careers-grid">
|
||||
<aside class="careers-filter" data-aos="fade-right">
|
||||
<span class="eyebrow">职位筛选</span>
|
||||
<div class="careers-search">
|
||||
<i class="fas fa-search"></i>
|
||||
<input v-model="keyword" placeholder="搜索职位、部门或城市" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-amber-50 rounded-xl p-6 mt-8"
|
||||
data-aos="fade-right"
|
||||
data-aos-delay="200"
|
||||
>
|
||||
<h3 class="font-medium text-gray-900 mb-4">加入我们的优势</h3>
|
||||
<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="careers-tabs">
|
||||
<button
|
||||
v-for="item in departments"
|
||||
:key="item"
|
||||
:class="{ active: department === item }"
|
||||
type="button"
|
||||
@click="department = item"
|
||||
>
|
||||
<div class="p-6">
|
||||
<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>
|
||||
{{ item }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<span class="text-sm bg-blue-500 text-white px-3 py-1 rounded-full">{{ currentJob.department }}</span>
|
||||
<span class="text-sm bg-amber-500 text-white px-3 py-1 rounded-full">{{ currentJob.type }}</span>
|
||||
<span class="text-sm bg-gray-700 text-white px-3 py-1 rounded-full">{{ currentJob.location }}</span>
|
||||
<div class="careers-contact-card">
|
||||
<h3>投递方式</h3>
|
||||
<p>请将简历发送至招聘邮箱,邮件标题注明职位名称。</p>
|
||||
<a :href="'mailto:' + contactEmail">{{ contactEmail }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 模态框内容 -->
|
||||
<div class="p-6 max-h-[70vh] overflow-y-auto">
|
||||
<!-- 职位详情 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="md:col-span-2">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-3">职位描述</h3>
|
||||
<p class="text-gray-700 mb-4">{{ currentJob.description }}</p>
|
||||
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-3">岗位要求</h3>
|
||||
<ul class="list-disc pl-5 space-y-2 text-gray-700">
|
||||
<li v-for="(requirement, idx) in currentJob.requirements" :key="idx">{{ requirement }}</li>
|
||||
</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 class="careers-list">
|
||||
<article v-for="job in filteredJobs" :key="job.id" class="career-job-card" data-aos="fade-up">
|
||||
<div>
|
||||
<span>{{ job.department }} / {{ job.city }}</span>
|
||||
<h2>{{ job.title }}</h2>
|
||||
<p>{{ job.description }}</p>
|
||||
<div class="career-job-card__tags">
|
||||
<em>{{ job.job_type }}</em>
|
||||
<em>{{ job.salary }}</em>
|
||||
<em>{{ job.experience }}</em>
|
||||
<em v-if="job.featured">重点招聘</em>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<button class="secondary-button" type="button" @click="openJob(job)">查看详情</button>
|
||||
</article>
|
||||
<div v-if="!filteredJobs.length" class="careers-empty">暂无匹配职位,请换个关键词试试。</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SectionTitle from '@/components/ui/SectionTitle.vue'
|
||||
import Card from '@/components/ui/Card.vue'
|
||||
import {nextTick, onMounted, ref} from 'vue'
|
||||
import AOS from 'aos'
|
||||
import 'aos/dist/aos.css'
|
||||
import MapContainer from "@/components/ui/MapContainer.vue";
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import MapContainer from '@/components/ui/MapContainer.vue'
|
||||
import { getRecruitJobs, getSiteSettings } from '@/api/request/site'
|
||||
|
||||
// // 初始化AOS
|
||||
// onMounted(() => {
|
||||
// AOS.init({
|
||||
// duration: 800,
|
||||
// offset: 100,
|
||||
// easing: 'ease-in-out',
|
||||
// once: true
|
||||
// })
|
||||
//
|
||||
// })
|
||||
const keyword = ref('')
|
||||
const department = ref('全部')
|
||||
const jobs = ref([])
|
||||
const siteSettings = ref({})
|
||||
const currentJob = ref(null)
|
||||
|
||||
const position = {
|
||||
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([
|
||||
const fallbackJobs = [
|
||||
{
|
||||
id: 1,
|
||||
title: '高级前端开发工程师',
|
||||
department: '技术部',
|
||||
type: '全职',
|
||||
location: '上海',
|
||||
date: '2023-07-10',
|
||||
job_type: '全职',
|
||||
city: '杭州',
|
||||
salary: '25K-45K',
|
||||
experience: '5年以上',
|
||||
featured: true,
|
||||
// 新增字段
|
||||
description: '负责公司核心产品的前端架构设计与开发,优化用户体验和前端性能,与产品、设计团队紧密合作实现产品需求。',
|
||||
requirements: [
|
||||
'精通Vue.js框架及其生态系统',
|
||||
'熟悉前端工程化、模块化开发',
|
||||
'有大型项目架构设计经验',
|
||||
'良好的团队协作和沟通能力',
|
||||
'计算机相关专业本科以上学历'
|
||||
],
|
||||
description: '负责官网、后台和小程序前端体验,建立稳定的组件体系。',
|
||||
requirements: ['熟悉 Vue 3 与工程化', '有中后台或 SaaS 项目经验', '关注性能和可访问性'],
|
||||
manager: '张经理',
|
||||
managerTitle: '技术总监',
|
||||
email: 'tech@shyoubai.com',
|
||||
phone: '021-34161621',
|
||||
photoRequirement: '请提供近期免冠证件照(白底)和生活照各一张,尺寸为2寸,文件大小不超过5MB。'
|
||||
},
|
||||
{
|
||||
title: '行业研究员',
|
||||
department: '研究部',
|
||||
type: '全职',
|
||||
location: '上海',
|
||||
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。'
|
||||
},
|
||||
// 其他职位类似添加...
|
||||
])
|
||||
manager_title: '技术负责人',
|
||||
email: 'tech@xiaokang.example',
|
||||
phone: '0571-00000000'
|
||||
}
|
||||
]
|
||||
const steps = [
|
||||
{ title: '简历投递', desc: '通过邮箱或后台职位入口提交简历。' },
|
||||
{ title: '初步沟通', desc: '确认经历、意向和岗位匹配度。' },
|
||||
{ title: '专业面试', desc: '围绕真实业务问题展开交流。' },
|
||||
{ title: '录用沟通', desc: '明确薪酬、职责和入职安排。' }
|
||||
]
|
||||
|
||||
const contactEmail = computed(() => siteSettings.value.contact_email || 'hr@xiaokang.example')
|
||||
const departments = computed(() => ['全部', ...new Set(jobs.value.map(job => job.department).filter(Boolean))])
|
||||
const filteredJobs = computed(() => jobs.value.filter((job) => {
|
||||
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
|
||||
}))
|
||||
|
||||
const openJob = (job) => { currentJob.value = job }
|
||||
|
||||
onMounted(async () => {
|
||||
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
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 校招计划中的SectionTitle需要调整文字颜色 */
|
||||
.text-white ::v-deep .section-title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.text-white ::v-deep .section-subtitle {
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.text-white ::v-deep .section-divider {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
/* 地图容器样式 */
|
||||
#map-container {
|
||||
min-height: 400px;
|
||||
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>
|
||||
|
||||
@@ -1,514 +1,71 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 页面标题 - 使用公司文化背景图 -->
|
||||
<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">
|
||||
<template>
|
||||
<div class="page-shell refined-page features-refined">
|
||||
<section class="subpage-hero">
|
||||
<div class="container mx-auto px-4">
|
||||
<SectionTitle
|
||||
title="学习文化"
|
||||
subtitle="研究与投资的持久战"
|
||||
description="保持学习,不断思考是持久战的基础"
|
||||
align="center"
|
||||
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 class="subpage-hero__grid">
|
||||
<div data-aos="fade-up">
|
||||
<span class="subpage-kicker">PRODUCT CAPABILITY</span>
|
||||
<h1>把诊所的每个角色,放进同一套工作秩序。</h1>
|
||||
<p>产品能力页专注说明模块边界:患者端、医生端、老板端、推广员端和 PC 后台如何协作,而不是重复首页叙事。</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative rounded-xl overflow-hidden shadow-lg"
|
||||
data-aos="fade-left"
|
||||
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 class="capability-map" data-aos="fade-left">
|
||||
<div v-for="node in capabilityNodes" :key="node.title" :class="node.class">
|
||||
<span>{{ node.label }}</span>
|
||||
<strong>{{ node.title }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 团队理念部分:大道不孤、众行致远 -->
|
||||
<section class="py-16 md:py-24">
|
||||
<section class="editorial-section">
|
||||
<div class="container mx-auto px-4">
|
||||
<SectionTitle
|
||||
title="团队理念"
|
||||
subtitle="大道不孤,众行致远"
|
||||
description="在酉佰,我们坚信真正的成功来自团队协作与共同成长"
|
||||
align="center"
|
||||
data-aos="fade-up"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="bg-white rounded-2xl shadow-xl overflow-hidden mt-16"
|
||||
data-aos="fade-up"
|
||||
data-aos-delay="200"
|
||||
>
|
||||
<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 class="feature-split">
|
||||
<aside data-aos="fade-right">
|
||||
<span class="eyebrow">能力地图</span>
|
||||
<h2>先定义角色,再定义数据如何流动。</h2>
|
||||
<p>诊所经营的混乱,往往来自角色之间的断点。萧康云医用端口拆分工作,用后台统一沉淀。</p>
|
||||
</aside>
|
||||
<div class="role-matrix">
|
||||
<article v-for="role in roles" :key="role.title" data-aos="fade-up">
|
||||
<span>{{ role.index }}</span>
|
||||
<h3>{{ role.title }}</h3>
|
||||
<p>{{ role.description }}</p>
|
||||
<ul><li v-for="item in role.items" :key="item">{{ item }}</li></ul>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 循序渐进部分 -->
|
||||
<section class="py-16 md:py-24 bg-gradient-to-r from-amber-50 to-amber-100">
|
||||
<section class="editorial-section editorial-muted">
|
||||
<div class="container mx-auto px-4">
|
||||
<SectionTitle
|
||||
title="循序渐进"
|
||||
subtitle="顺其自然,脚踏实地"
|
||||
description="不盲目追求无用的结果,一步一步向前迈进"
|
||||
align="center"
|
||||
data-aos="fade-up"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="max-w-4xl mx-auto mt-16"
|
||||
data-aos="fade-up"
|
||||
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 class="process-slice">
|
||||
<div data-aos="fade-up">
|
||||
<span class="eyebrow">流程切片</span>
|
||||
<h2>从线索到复诊,每一步都有明确承接人。</h2>
|
||||
</div>
|
||||
<div class="process-slice__rail">
|
||||
<article v-for="step in steps" :key="step.title" data-aos="fade-up">
|
||||
<span>{{ step.index }}</span>
|
||||
<h3>{{ step.title }}</h3>
|
||||
<p>{{ step.description }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 文化实践案例 -->
|
||||
<section class="py-16 md:py-24">
|
||||
<section class="editorial-section">
|
||||
<div class="container mx-auto px-4">
|
||||
<SectionTitle
|
||||
title="文化实践"
|
||||
subtitle="理念落地实例"
|
||||
description="学习与渐进理念在实际工作中的体现"
|
||||
align="center"
|
||||
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 class="backend-panel" data-aos="zoom-in">
|
||||
<div>
|
||||
<span class="eyebrow">PC 后台</span>
|
||||
<h2>后台不是菜单集合,而是权限、内容和商业化的控制面。</h2>
|
||||
<p>页面模块、素材、招聘、套餐、订单、角色菜单都由后台管理,为后续数据库驱动和运营迭代留出空间。</p>
|
||||
</div>
|
||||
|
||||
<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 class="backend-panel__preview">
|
||||
<div v-for="item in backendItems" :key="item.title"><strong>{{ item.title }}</strong><span>{{ item.desc }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -517,26 +74,29 @@
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
@@ -1,284 +1,93 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 主视觉区域 -->
|
||||
<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>
|
||||
<template>
|
||||
<div class="page-shell rich-home">
|
||||
<HeroCinematic />
|
||||
|
||||
<div class="md:w-1/2 flex justify-center">
|
||||
<div class="relative">
|
||||
<div class="absolute -top-6 -right-6 w-64 h-64 bg-amber-500 rounded-full opacity-10" data-aos="fade"></div>
|
||||
<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
|
||||
class="relative z-10 rounded-2xl overflow-hidden shadow-xl w-full max-w-md"
|
||||
data-aos="zoom-in"
|
||||
data-aos-delay="200"
|
||||
>
|
||||
<!-- 图片占位 -->
|
||||
<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>
|
||||
<section class="motion-section home-brief">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="home-brief__grid">
|
||||
<div data-aos="fade-right">
|
||||
<span class="eyebrow">官网首页</span>
|
||||
<h2>不重复讲功能,先让经营者看见系统进入诊所的方式。</h2>
|
||||
<p>首页负责建立第一层判断:萧康云医能把获客、服务、复诊和经营复盘连起来。具体模块、阶段方案和价格,交给各自页面展开。</p>
|
||||
</div>
|
||||
<div class="home-brief__metrics">
|
||||
<MetricArtCard v-for="metric in metrics" :key="metric.label" v-bind="metric" />
|
||||
</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"
|
||||
/>
|
||||
<ClinicJourney />
|
||||
<RoleConstellation />
|
||||
<DataCurrent />
|
||||
<ScenarioShowcase />
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-8 mt-16">
|
||||
<Card
|
||||
v-for="(value, index) in values"
|
||||
:key="index"
|
||||
:title="value.title"
|
||||
:icon="value.icon"
|
||||
:data-aos="'fade-up'"
|
||||
:data-aos-delay="index * 100"
|
||||
<section class="motion-section">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="section-intro" data-aos="fade-up">
|
||||
<span class="eyebrow">首页分工</span>
|
||||
<h2>把信息拆到正确的位置,用户才不会在首页迷路。</h2>
|
||||
</div>
|
||||
<div class="editorial-card-grid">
|
||||
<EditorialCard
|
||||
v-for="card in pageGuides"
|
||||
:key="card.title"
|
||||
v-bind="card"
|
||||
>
|
||||
<p>
|
||||
{{ value.description }}
|
||||
</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>
|
||||
<router-link :to="card.link" class="text-sm font-bold text-[var(--gold)]">进入页面</router-link>
|
||||
</EditorialCard>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 企业优势 -->
|
||||
<section class="py-16 md:py-24 bg-gray-50">
|
||||
<section class="motion-section editorial-muted">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex flex-col lg:flex-row items-center justify-between">
|
||||
<div class="lg:w-5/12 mb-12 lg:mb-0">
|
||||
<SectionTitle
|
||||
title="为什么选择"
|
||||
subtitle="企业优势"
|
||||
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 class="home-flow">
|
||||
<div data-aos="fade-right">
|
||||
<span class="eyebrow">上线路径</span>
|
||||
<h2>从一张官网线索,到一套可运营的系统。</h2>
|
||||
<p>按真实门店节奏落地,而不是一次性把所有功能塞给团队。</p>
|
||||
</div>
|
||||
|
||||
<div class="lg:w-6/12" data-aos="fade-left" data-aos-delay="200">
|
||||
<div class="relative">
|
||||
<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 class="relative bg-white rounded-2xl overflow-hidden shadow-lg flex">
|
||||
<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"
|
||||
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>
|
||||
<ol>
|
||||
<li v-for="item in rollout" :key="item.title" data-aos="fade-up">
|
||||
<span>{{ item.index }}</span>
|
||||
<div>
|
||||
<h3>{{ item.title }}</h3>
|
||||
<p>{{ item.description }}</p>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
<!-- 客户评价 -->
|
||||
<section class="py-16 md:py-24">
|
||||
<section class="motion-section final-cta">
|
||||
<div class="container mx-auto px-4">
|
||||
<SectionTitle
|
||||
title="客户评价"
|
||||
subtitle="他们这样说"
|
||||
description="多年来,我们服务的客户遍布不同行业,获得了广泛认可"
|
||||
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 class="final-cta__inner" data-aos="zoom-in">
|
||||
<div>
|
||||
<span class="eyebrow">下一步</span>
|
||||
<h2>先看一次真实流程,再决定萧康云医如何进入你的诊所。</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 合作品牌 -->
|
||||
<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 class="flex flex-col gap-3 sm:flex-row">
|
||||
<router-link to="/pricing" class="primary-button justify-center">预约演示</router-link>
|
||||
<router-link to="/solutions" class="secondary-button justify-center">查看方案</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -287,324 +96,36 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SectionTitle from '@/components/ui/SectionTitle.vue'
|
||||
import Card from '@/components/ui/Card.vue'
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import HeroCinematic from '@/components/home/HeroCinematic.vue'
|
||||
import ClinicJourney from '@/components/home/ClinicJourney.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 = [
|
||||
{
|
||||
title: '创新驱动',
|
||||
icon: 'fas fa-lightbulb',
|
||||
description: '不断探索新技术、新思路,为客户提供前沿解决方案'
|
||||
},
|
||||
{
|
||||
title: '客户至上',
|
||||
icon: 'fas fa-handshake',
|
||||
description: '深度理解客户需求,提供定制化服务与全流程支持'
|
||||
},
|
||||
{
|
||||
title: '专业可靠',
|
||||
icon: 'fas fa-medal',
|
||||
description: '高素质专业团队,严谨的工作态度,确保项目高质量交付'
|
||||
}
|
||||
const metrics = [
|
||||
{ label: '第一层', value: '入口', description: '官网只负责让诊所愿意开始对话。' },
|
||||
{ label: '第二层', value: '流程', description: '把线索、预约、接诊和复诊串起来。' },
|
||||
{ label: '第三层', value: '判断', description: '让老板看到经营动作背后的结果。' }
|
||||
]
|
||||
|
||||
const stats = [
|
||||
{ value: '12+', label: '行业经验' },
|
||||
{ value: '200+', label: '合作企业' },
|
||||
{ value: '50+', label: '专业团队' },
|
||||
{ value: '98%', label: '客户满意度' }
|
||||
const pageGuides = [
|
||||
{ index: '01', eyebrow: '产品能力', title: '看模块如何协同', description: '适合已经理解需求、想确认系统边界的人。', link: '/features', tone: 'paper' },
|
||||
{ index: '02', eyebrow: '解决方案', title: '看阶段如何落地', description: '适合单店起步、增长运营和连锁预留的不同诊所。', link: '/solutions', tone: 'line' },
|
||||
{ index: '03', eyebrow: '价格', title: '看投入如何选择', description: '适合需要比较月付、年付和套餐权益的人。', link: '/pricing', tone: 'warm' }
|
||||
]
|
||||
|
||||
const advantages = [
|
||||
{
|
||||
title: '行业专业知识',
|
||||
icon: 'fas fa-book',
|
||||
description: '深耕行业多年的专家团队,掌握行业发展趋势'
|
||||
},
|
||||
{
|
||||
title: '技术创新',
|
||||
icon: 'fas fa-cogs',
|
||||
description: '持续投资技术研发,引领行业技术创新'
|
||||
},
|
||||
{
|
||||
title: '定制解决方案',
|
||||
icon: 'fas fa-clipboard-list',
|
||||
description: '根据企业需求量身定制解决方案'
|
||||
},
|
||||
const rollout = [
|
||||
{ index: '01', title: '梳理经营入口', description: '确认官网、推广码、电话咨询和到店登记如何进入同一套线索池。' },
|
||||
{ index: '02', title: '配置角色端口', description: '按诊所团队结构启用患者端、医生端、老板端、推广员端和 PC 后台。' },
|
||||
{ index: '03', title: '跑通服务闭环', description: '让预约、接诊、随访和复诊提醒先在真实门店流程里顺起来。' },
|
||||
{ index: '04', title: '沉淀经营判断', description: '用线索来源、转化、复购和订单数据帮助老板做每周经营复盘。' }
|
||||
]
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
content: '上海酉佰团队的专业性和创新思维帮助我们完成了数字化转型,大幅提升了工作效率,为业务增长提供了强大支持。',
|
||||
name: '张先生',
|
||||
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 faqs = [
|
||||
{ question: '首页为什么不把所有功能一次讲完?', answer: '首页负责建立理解和信任,具体模块能力、阶段方案和价格分别进入产品能力页、解决方案页和定价页。' },
|
||||
{ question: '后台是否已经有前端入口?', answer: '后台使用 /admin 分组路由,登录后进入仪表盘,并从后端菜单接口渲染多级菜单。' },
|
||||
{ question: '动效会影响移动端性能吗?', answer: '动效以 CSS、SVG、AOS 和 ECharts 轻量实现,并提供 reduced-motion 降级。' }
|
||||
]
|
||||
|
||||
// 生成合作伙伴数据(实际项目应使用真实数据)
|
||||
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>
|
||||
|
||||
<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
108
src/views/Pricing.vue
Normal 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
56
src/views/Solutions.vue
Normal 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>
|
||||
@@ -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">
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">技术专家团队</h3>
|
||||
<p class="text-gray-600 mb-6">
|
||||
酉佰佰是一支秉承着"开放透明、纪律严明、听从指挥、主观能动"管理风格的团队。
|
||||
萧康云医是一支秉承着"开放透明、纪律严明、听从指挥、主观能动"管理风格的团队。
|
||||
我们遵循循序渐进,但绝不放任自流的管理理念。
|
||||
</p>
|
||||
<p class="text-gray-600 mb-6">
|
||||
@@ -118,7 +118,7 @@
|
||||
<SectionTitle
|
||||
title="发展规划"
|
||||
subtitle="未来愿景"
|
||||
description="酉佰佰团队的战略发展规划"
|
||||
description="萧康云医团队的战略发展规划"
|
||||
align="center"
|
||||
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">
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">五年发展规划</h3>
|
||||
<p class="text-gray-600 mb-6">
|
||||
酉佰佰团队刚经历了第一个五年的发展,展望未来,公司一方面致力于在宏观弱势期进行人才积累,
|
||||
萧康云医团队刚经历了第一个阶段的发展,展望未来,公司一方面致力于在医疗数字化场景中进行人才积累,
|
||||
另一方面也在积极地进行政治经济研究、产业企业研究、风险管理研究和投资等投资公司核心竞争力的孵化与锻炼。
|
||||
</p>
|
||||
<p class="text-gray-600 mb-6">
|
||||
@@ -254,7 +254,7 @@
|
||||
<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>
|
||||
<p class="text-xl text-blue-300 mb-8">
|
||||
如果你才华横溢、充满激情、渴望成长,我们欢迎你加入酉佰佰大家庭
|
||||
如果你才华横溢、充满激情、渴望成长,我们欢迎你加入萧康云医大家庭
|
||||
</p>
|
||||
<router-link
|
||||
to="/careers"
|
||||
|
||||
73
src/views/admin/AdminDashboard.vue
Normal file
73
src/views/admin/AdminDashboard.vue
Normal 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>
|
||||
25
src/views/admin/AdminJobs.vue
Normal file
25
src/views/admin/AdminJobs.vue
Normal 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>
|
||||
36
src/views/admin/AdminLeads.vue
Normal file
36
src/views/admin/AdminLeads.vue
Normal 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>
|
||||
28
src/views/admin/AdminLogin.vue
Normal file
28
src/views/admin/AdminLogin.vue
Normal 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>
|
||||
23
src/views/admin/AdminMedia.vue
Normal file
23
src/views/admin/AdminMedia.vue
Normal 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>管理 LOGO、favicon/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>
|
||||
36
src/views/admin/AdminMenus.vue
Normal file
36
src/views/admin/AdminMenus.vue
Normal 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>
|
||||
37
src/views/admin/AdminOrders.vue
Normal file
37
src/views/admin/AdminOrders.vue
Normal 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>
|
||||
35
src/views/admin/AdminPlans.vue
Normal file
35
src/views/admin/AdminPlans.vue
Normal 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>
|
||||
40
src/views/admin/AdminProfile.vue
Normal file
40
src/views/admin/AdminProfile.vue
Normal 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>响应固定为 code、message、result。</li>
|
||||
<li>JSON 字段统一 snake_case,例如 order_no、menu_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>
|
||||
30
src/views/admin/AdminRoles.vue
Normal file
30
src/views/admin/AdminRoles.vue
Normal 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>
|
||||
37
src/views/admin/AdminSections.vue
Normal file
37
src/views/admin/AdminSections.vue
Normal 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>
|
||||
18
src/views/admin/AdminSiteSettings.vue
Normal file
18
src/views/admin/AdminSiteSettings.vue
Normal 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>控制全站标题、LOGO、ico、默认 SEO、GSO/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>
|
||||
151
src/views/admin/AdminTablePage.vue
Normal file
151
src/views/admin/AdminTablePage.vue
Normal 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>
|
||||
@@ -19,7 +19,7 @@ export default defineConfig({
|
||||
proxy: {
|
||||
'/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,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user