For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Extend User entity with vendor/service provider fields, add VendorController for vendor application flow, change TabBar from 首页/任务/心愿单/我的 to 首页/发现/商城/我的, create placeholder pages, add membership banner to homepage, and add vendor review page to Web admin.
Architecture: Phase 0 is the foundation — all subsequent phases depend on the role model extension and TabBar skeleton. Backend: add 4 fields to User + DatabaseInitializer migration + new VendorController/VendorService. Frontend: change pages.json TabBar + 3 new placeholder pages + update profile + update index. Web: add vendor review route + page.
Tech Stack: Java 1.8 + Spring Boot 2.7 + MyBatis-Plus + uni-app Vue 2 + Element UI
Spec reference: docs/superpowers/specs/2026-06-02-care-family-club-design.md (溪艾福 Care Family Club / CFC)
Key constraints:
java.util.Date and implements Serializable@RequestMapping("/api/...") + @PostMapping@Resource?.Files:
Modify: zxyj-backend/src/main/java/com/zxyj/entity/User.java
[ ] Step 1: Add 4 fields to User.java
Add after the existing familyRole field (before createdAt):
// 服务商类型: null=普通用户 / planner / activity_provider / product_supplier
private String vendorType;
// 服务商审核状态: null=未申请 / pending / approved / rejected
private String vendorStatus;
// 服务商审核拒绝原因
private String vendorRejectReason;
// 家庭管理员标记
private Boolean isFamilyAdmin;
Run: cd zxyj-backend && mvn clean compile -q
Expected: BUILD SUCCESS
Files:
Modify: zxyj-backend/src/main/java/com/zxyj/config/DatabaseInitializer.java
[ ] Step 1: Find the runMigrations() method and add ALTER TABLE statements
Search for private void runMigrations() in DatabaseInitializer.java. Add at the end of the migration SQL list:
// Phase 0: 溪福俱乐部 — 服务商字段
"ALTER TABLE users ADD COLUMN IF NOT EXISTS vendor_type VARCHAR(32) DEFAULT NULL COMMENT '服务商类型'",
"ALTER TABLE users ADD COLUMN IF NOT EXISTS vendor_status VARCHAR(16) DEFAULT NULL COMMENT '服务商审核状态'",
"ALTER TABLE users ADD COLUMN IF NOT EXISTS vendor_reject_reason VARCHAR(500) DEFAULT NULL COMMENT '审核拒绝原因'",
"ALTER TABLE users ADD COLUMN IF NOT EXISTS is_family_admin TINYINT DEFAULT 0 COMMENT '家庭管理员'",
If ADD COLUMN IF NOT EXISTS is not supported by the MySQL version (8.0 should support it), use the try-catch pattern that already exists in the file. Inspect existing migrations to see the pattern used.
At the end of runMigrations(), add data migration for existing teachers:
// Phase 0: 迁移已有规划师到服务商体系
jdbcTemplate.update(
"UPDATE users SET vendor_type = 'planner', vendor_status = 'approved' WHERE teacher_status = 'approved' AND vendor_type IS NULL"
);
Run: cd zxyj-backend && mvn clean compile -q
Expected: BUILD SUCCESS
Files:
zxyj-backend/src/main/java/com/zxyj/controller/vendor/VendorController.javazxyj-backend/src/main/java/com/zxyj/service/VendorService.javaModify: zxyj-backend/src/main/java/com/zxyj/mapper/UserMapper.java (only if methods are missing, likely already has basic CRUD)
[ ] Step 1: Create VendorService
package com.zxyj.service;
import com.zxyj.common.Result;
import com.zxyj.entity.User;
import com.zxyj.mapper.UserMapper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
@Service
public class VendorService {
@Resource
private UserMapper userMapper;
/**
* 提交服务商入驻申请
*/
public Result apply(Long userId, String vendorType, String vendorInfo) {
User user = userMapper.selectById(userId);
if (user == null) {
return Result.error("用户不存在");
}
if (user.getVendorStatus() != null && !"rejected".equals(user.getVendorStatus())) {
return Result.error("已有入驻申请,请勿重复提交");
}
user.setVendorType(vendorType);
user.setVendorStatus("pending");
user.setVendorRejectReason(null);
user.setUpdatedAt(new Date());
userMapper.updateById(user);
return Result.success("入驻申请已提交,请等待审核");
}
/**
* 查询入驻审核状态
*/
public Result getStatus(Long userId) {
User user = userMapper.selectById(userId);
if (user == null) {
return Result.error("用户不存在");
}
java.util.Map<String, Object> data = new java.util.HashMap<>();
data.put("vendorType", user.getVendorType());
data.put("vendorStatus", user.getVendorStatus());
data.put("vendorRejectReason", user.getVendorRejectReason());
return Result.success(data);
}
/**
* 获取/编辑服务商信息(仅供已通过服务商使用)
*/
public Result getVendorInfo(Long userId) {
User user = userMapper.selectById(userId);
if (user == null) {
return Result.error("用户不存在");
}
if (!"approved".equals(user.getVendorStatus())) {
return Result.error("服务商资格未通过审核");
}
java.util.Map<String, Object> data = new java.util.HashMap<>();
data.put("vendorType", user.getVendorType());
data.put("vendorInfo", user.getVendorInfo());
data.put("realName", user.getRealName());
data.put("phone", user.getPhone());
return Result.success(data);
}
}
[ ] Step 2: Create VendorController
package com.zxyj.controller.vendor;
import com.zxyj.common.Result;
import com.zxyj.service.VendorService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Map;
@RestController
@RequestMapping("/api/vendor")
public class VendorController {
@Resource
private VendorService vendorService;
/**
* 提交服务商入驻申请
* POST /api/vendor/apply
* Body: { "vendorType": "planner", "vendorInfo": "..." }
*/
@PostMapping("/apply")
public Result apply(@RequestBody Map<String, Object> params) {
Long userId = getCurrentUserId();
String vendorType = (String) params.get("vendorType");
String vendorInfo = (String) params.getOrDefault("vendorInfo", "");
return vendorService.apply(userId, vendorType, vendorInfo);
}
/**
* 查询入驻审核状态
* POST /api/vendor/status
*/
@PostMapping("/status")
public Result getStatus() {
Long userId = getCurrentUserId();
return vendorService.getStatus(userId);
}
/**
* 获取服务商信息
* POST /api/vendor/info
*/
@PostMapping("/info")
public Result getVendorInfo() {
Long userId = getCurrentUserId();
return vendorService.getVendorInfo(userId);
}
/**
* 从请求头获取当前用户ID
* 与现有AuthController中模式一致
*/
private Long getCurrentUserId() {
String userIdStr = org.springframework.web.context.request.RequestContextHolder
.currentRequestAttributes()
.getRequest()
.getHeader("X-User-Id");
return userIdStr != null ? Long.parseLong(userIdStr) : null;
}
}
[ ] Step 3: Verify compilation
Run: cd zxyj-backend && mvn clean compile -q
Check: The new directory controller/vendor/ is picked up by Spring component scan. Verify no bean name conflicts.
Files:
Modify: zxyj-backend/src/main/java/com/zxyj/controller/admin/AdminController.java
[ ] Step 1: Add admin vendor review endpoints to AdminController
Find the AdminController file. Add the following methods:
/**
* 服务商入驻申请列表
* POST /api/admin/vendor/list
*/
@PostMapping("/vendor/list")
public Result vendorList(@RequestBody Map<String, Object> params) {
// 查询所有vendor_status非空的用户
Integer page = (Integer) params.getOrDefault("page", 1);
Integer size = (Integer) params.getOrDefault("size", 20);
String status = (String) params.get("status"); // pending / approved / rejected / null=全部
// 使用MyBatis-Plus QueryWrapper
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User> wrapper = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
wrapper.isNotNull("vendor_type");
if (status != null && !status.isEmpty()) {
wrapper.eq("vendor_status", status);
}
wrapper.orderByDesc("updated_at");
com.baomidou.mybatisplus.extension.plugins.pagination.Page<User> pageObj = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, size);
com.baomidou.mybatisplus.core.metadata.IPage<User> result = userMapper.selectPage(pageObj, wrapper);
java.util.Map<String, Object> data = new java.util.HashMap<>();
data.put("list", result.getRecords());
data.put("total", result.getTotal());
data.put("page", page);
data.put("size", size);
return Result.success(data);
}
/**
* 审核服务商入驻
* POST /api/admin/vendor/review
* Body: { "userId": 1, "action": "approve" / "reject", "reason": "拒绝原因" }
*/
@PostMapping("/vendor/review")
public Result vendorReview(@RequestBody Map<String, Object> params) {
Long userId = Long.valueOf(params.get("userId").toString());
String action = (String) params.get("action");
String reason = (String) params.getOrDefault("reason", "");
User user = userMapper.selectById(userId);
if (user == null) {
return Result.error("用户不存在");
}
if ("approve".equals(action)) {
user.setVendorStatus("approved");
user.setVendorRejectReason(null);
} else if ("reject".equals(action)) {
user.setVendorStatus("rejected");
user.setVendorRejectReason(reason);
} else {
return Result.error("无效的审核操作");
}
user.setUpdatedAt(new Date());
userMapper.updateById(user);
return Result.success("审核完成");
}
Add the required imports at the top:
import com.zxyj.entity.User;
import com.zxyj.mapper.UserMapper;
import java.util.Date;
And confirm @Resource private UserMapper userMapper; is present in the class.
Run: cd zxyj-backend && mvn clean compile -q
Expected: BUILD SUCCESS
Files:
Modify: zxyj-frontend/utils/api.js
[ ] Step 1: Add vendor API calls to api.js
Find the end of file. Add:
// ===================== 溪福俱乐部 - 服务商模块 =====================
// 提交服务商入驻申请
export const vendorApply = (data) => {
return request('/api/vendor/apply', 'POST', data)
}
// 查询入驻审核状态
export const vendorStatus = () => {
return request('/api/vendor/status', 'POST', {})
}
// 获取服务商信息
export const vendorInfo = () => {
return request('/api/vendor/info', 'POST', {})
}
Files:
Modify: zxyj-frontend/pages.json
[ ] Step 1: Change TabBar list and add new page registrations
Replace the tabBar.list with:
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#FF6B6B",
"borderStyle": "black",
"backgroundColor": "#FFFFFF",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页"
},
{
"pagePath": "pages/discover/index",
"text": "发现"
},
{
"pagePath": "pages/shop/index",
"text": "商城"
},
{
"pagePath": "pages/profile/profile",
"text": "我的"
}
]
}
Add new page registrations in the pages array (before the globalStyle section). Place after the last existing entry:
// ===== 溪福俱乐部 Phase 0 =====
{
"path": "pages/discover/index",
"style": {
"navigationBarTitleText": "发现"
}
},
{
"path": "pages/shop/index",
"style": {
"navigationBarTitleText": "商城"
}
},
{
"path": "pages/vendor/apply",
"style": {
"navigationBarTitleText": "服务商入驻"
}
},
{
"path": "pages/vendor/center",
"style": {
"navigationBarTitleText": "服务商中心"
}
}
Files:
Create: zxyj-frontend/pages/discover/index.vue
[ ] Step 1: Create discover placeholder page
<template>
<view class="container">
<view class="placeholder">
<text class="icon">🔍</text>
<text class="title">发现</text>
<text class="desc">活动与课程即将上线,敬请期待</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {}
},
onLoad() {}
}
</script>
<style>
.container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #f8f8f8;
}
.placeholder {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 40rpx;
}
.icon {
font-size: 120rpx;
margin-bottom: 30rpx;
}
.title {
font-size: 40rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.desc {
font-size: 28rpx;
color: #999;
text-align: center;
}
</style>
Files:
Create: zxyj-frontend/pages/shop/index.vue
[ ] Step 1: Create shop placeholder page
<template>
<view class="container">
<view class="placeholder">
<text class="icon">🛒</text>
<text class="title">商城</text>
<text class="desc">商品与服务即将上线,敬请期待</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {}
},
onLoad() {}
}
</script>
<style>
.container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #f8f8f8;
}
.placeholder {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 40rpx;
}
.icon {
font-size: 120rpx;
margin-bottom: 30rpx;
}
.title {
font-size: 40rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.desc {
font-size: 28rpx;
color: #999;
text-align: center;
}
</style>
Files:
Create: zxyj-frontend/pages/vendor/apply.vue
[ ] Step 1: Create vendor application form page
<template>
<view class="container">
<view class="header">
<text class="title">服务商入驻</text>
<text class="subtitle">选择服务商类型,提交入驻申请</text>
</view>
<view class="form">
<view class="form-item">
<text class="label">服务商类型</text>
<picker mode="selector" :range="vendorTypes" range-key="label" @change="onTypeChange">
<view class="picker">
<text v-if="formData.vendorType">{{ getTypeLabel(formData.vendorType) }}</text>
<text v-else class="placeholder">请选择服务商类型</text>
<text class="arrow">›</text>
</view>
</picker>
</view>
<view class="form-item">
<text class="label">资质说明</text>
<textarea
class="textarea"
v-model="formData.vendorInfo"
placeholder="请填写您的资质信息、服务经验等"
maxlength="500"
/>
<text class="counter">{{ formData.vendorInfo.length }}/500</text>
</view>
<button class="btn-submit" :disabled="!formData.vendorType || submitting" @click="handleSubmit">
{{ submitting ? '提交中...' : '提交申请' }}
</button>
</view>
</view>
</template>
<script>
import { vendorApply } from '../../utils/api'
export default {
data() {
return {
vendorTypes: [
{ value: 'planner', label: '成长规划师' },
{ value: 'activity_provider', label: '活动提供商' },
{ value: 'product_supplier', label: '商品供应商' }
],
formData: {
vendorType: '',
vendorInfo: ''
},
submitting: false
}
},
methods: {
onTypeChange(e) {
this.formData.vendorType = this.vendorTypes[e.detail.value].value
},
getTypeLabel(value) {
const item = this.vendorTypes.find(t => t.value === value)
return item ? item.label : value
},
async handleSubmit() {
this.submitting = true
try {
const res = await vendorApply({
vendorType: this.formData.vendorType,
vendorInfo: this.formData.vendorInfo
})
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (e) {
// api.js already shows error toast
} finally {
this.submitting = false
}
}
}
}
</script>
<style>
.container {
padding: 30rpx;
background: #f8f8f8;
min-height: 100vh;
}
.header {
text-align: center;
padding: 40rpx 0;
}
.title {
font-size: 40rpx;
font-weight: bold;
color: #333;
}
.subtitle {
font-size: 26rpx;
color: #999;
margin-top: 10rpx;
display: block;
}
.form {
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
}
.form-item {
margin-bottom: 30rpx;
}
.label {
font-size: 28rpx;
color: #333;
font-weight: bold;
display: block;
margin-bottom: 15rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 2rpx solid #eee;
}
.placeholder {
color: #ccc;
}
.arrow {
color: #ccc;
font-size: 36rpx;
}
.textarea {
width: 100%;
height: 200rpx;
padding: 20rpx;
border: 2rpx solid #eee;
border-radius: 10rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.counter {
display: block;
text-align: right;
font-size: 24rpx;
color: #ccc;
margin-top: 10rpx;
}
.btn-submit {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: linear-gradient(135deg, #FF6B6B, #FF8E53);
color: #fff;
border-radius: 44rpx;
font-size: 32rpx;
margin-top: 40rpx;
}
.btn-submit[disabled] {
opacity: 0.5;
}
</style>
Files:
Create: zxyj-frontend/pages/vendor/center.vue
[ ] Step 1: Create vendor center page
<template>
<view class="container">
<view class="header">
<text class="icon">🏪</text>
<text class="title">服务商中心</text>
</view>
<view class="info-card" v-if="vendorInfoData">
<view class="info-row">
<text class="info-label">服务商类型</text>
<text class="info-value">{{ getTypeLabel(vendorInfoData.vendorType) }}</text>
</view>
<view class="info-row">
<text class="info-label">真实姓名</text>
<text class="info-value">{{ vendorInfoData.realName || '未设置' }}</text>
</view>
<view class="info-row">
<text class="info-label">联系电话</text>
<text class="info-value">{{ vendorInfoData.phone || '未设置' }}</text>
</view>
</view>
<view class="menu-list">
<view class="menu-item" @click="goToProductManage">
<text>📦 商品管理</text>
<text class="arrow">›</text>
</view>
<view class="menu-item" @click="goToOrderManage">
<text>📋 订单管理</text>
<text class="arrow">›</text>
</view>
</view>
<view class="coming-soon">
<text>更多功能即将上线...</text>
</view>
</view>
</template>
<script>
import { vendorInfo } from '../../utils/api'
export default {
data() {
return {
vendorTypes: [
{ value: 'planner', label: '成长规划师' },
{ value: 'activity_provider', label: '活动提供商' },
{ value: 'product_supplier', label: '商品供应商' }
],
vendorInfoData: null
}
},
onLoad() {
this.loadVendorInfo()
},
methods: {
getTypeLabel(value) {
const item = this.vendorTypes.find(t => t.value === value)
return item ? item.label : value
},
async loadVendorInfo() {
try {
const res = await vendorInfo()
this.vendorInfoData = res.data
} catch (e) {
// handled by api.js
}
},
goToProductManage() {
uni.showToast({ title: '即将上线', icon: 'none' })
},
goToOrderManage() {
uni.showToast({ title: '即将上线', icon: 'none' })
}
}
}
</script>
<style>
.container {
padding: 30rpx;
background: #f8f8f8;
min-height: 100vh;
}
.header {
text-align: center;
padding: 40rpx 0;
display: flex;
flex-direction: column;
align-items: center;
}
.icon {
font-size: 100rpx;
margin-bottom: 20rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.info-card {
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 15rpx 0;
border-bottom: 2rpx solid #f5f5f5;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-size: 28rpx;
color: #999;
}
.info-value {
font-size: 28rpx;
color: #333;
}
.menu-list {
background: #fff;
border-radius: 20rpx;
}
.menu-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 2rpx solid #f5f5f5;
font-size: 30rpx;
}
.menu-item:last-child {
border-bottom: none;
}
.arrow {
color: #ccc;
font-size: 36rpx;
}
.coming-soon {
text-align: center;
padding: 60rpx;
color: #ccc;
font-size: 26rpx;
}
</style>
Files:
Modify: zxyj-frontend/pages/profile/profile.vue
[ ] Step 1: Add membership entry and vendor entry after "积分记录" menu item
Find the 📊 积分记录 menu-item block. Add after it:
<!-- 会员中心(溪艾福) -->
<view class="menu-item" @click="goToMembership">
<text>👑 会员中心</text>
<text class="arrow">›</text>
</view>
<!-- 服务商中心(仅已通过服务商可见) -->
<view class="menu-item" v-if="isVendor" @click="goToVendorCenter">
<text>🏪 服务商中心</text>
<text class="arrow">›</text>
</view>
isVendor data property and handler methodsIn the data() function, add:
isVendor: false,
In the methods section, add:
goToMembership() {
uni.showToast({ title: '即将上线', icon: 'none' })
},
goToVendorCenter() {
uni.navigateTo({ url: '/pages/vendor/center' })
}
In the onShow() lifecycle hook, add vendor status check:
// 检查服务商状态
this.checkVendorStatus()
Add the method:
async checkVendorStatus() {
try {
const { vendorStatus } = require('../../utils/api')
const res = await vendorStatus()
this.isVendor = res.data && res.data.vendorStatus === 'approved'
} catch (e) {
this.isVendor = false
}
},
Also add the import at top of <script> section:
import { vendorStatus } from '../../utils/api'
Files:
zxyj-frontend/pages/index/parent-index.vueModify: zxyj-frontend/pages/index/child-index.vue
[ ] Step 1: Add 五行能量沙盘 (pentagram) component after welcome section
Read the file first to find the exact insertion point. Add the sandbox component at the top of the content area:
<!-- 五行能量沙盘(溪艾福) -->
<view class="wuxing-sandbox">
<view class="sandbox-header">
<text class="sandbox-title">五行能量沙盘</text>
<text class="sandbox-badge">综合成长力 0%</text>
</view>
<view class="sandbox-star">
<!-- 五角星SVG/CSS布局 -->
<view class="star-point star-point-top" @click="goToDomain('mind')">
<text class="point-icon">🔥</text>
<text class="point-label">心·火</text>
<text class="point-status inactive">待激活</text>
</view>
<view class="star-point star-point-left" @click="goToDomain('action')">
<text class="point-icon">🌿</text>
<text class="point-label">行·木</text>
<text class="point-status inactive">待激活</text>
</view>
<view class="star-point star-point-right" @click="goToDomain('wealth')">
<text class="point-icon">💧</text>
<text class="point-label">富·水</text>
<text class="point-status inactive">待激活</text>
</view>
<view class="star-point star-point-bl" @click="goToDomain('wisdom')">
<text class="point-icon">⚔️</text>
<text class="point-label">智·金</text>
<text class="point-status inactive">待激活</text>
</view>
<view class="star-point star-point-br" @click="goToDomain('body')">
<text class="point-icon">🌏</text>
<text class="point-label">身·土</text>
<text class="point-status inactive">待激活</text>
</view>
<!-- 中心徽章 -->
<view class="star-center">
<text class="center-text">五行平衡</text>
<text class="center-sub">共同成长</text>
</view>
</view>
<!-- 相生指引 -->
<view class="shenke-hint">
<text class="hint-text">🌱 完成菌群检测,点亮「身·土」</text>
</view>
<!-- 维度状态条 -->
<view class="dimension-bar">
<view class="dim-item inactive">🌿行</view>
<text class="dim-arrow">→</text>
<view class="dim-item inactive">🔥心</view>
<text class="dim-arrow">→</text>
<view class="dim-item inactive">🌏身</view>
<text class="dim-arrow">→</text>
<view class="dim-item inactive">⚔️智</view>
<text class="dim-arrow">→</text>
<view class="dim-item inactive">💧富</view>
</view>
</view>
<!-- 会员Banner(溪艾福) -->
<view class="membership-banner" @click="goToMembership">
<view class="banner-content">
<text class="banner-icon">👑</text>
<view class="banner-text">
<text class="banner-title">{{ membershipText }}</text>
<text class="banner-subtitle">点击查看会员权益</text>
</view>
<text class="banner-arrow">›</text>
</view>
</view>
Note: The pentagram layout uses position: absolute/fixed inside a relative container. The five star-point-* classes position each point according to a regular pentagram. Use display: flex; justify-content: center; for horizontal centering of the star-center.
In data():
membershipText: '开通家庭会员 畅享全部权益',
In methods():
goToDomain(domain) {
uni.showToast({ title: '即将上线', icon: 'none' })
},
goToMembership() {
uni.showToast({ title: '即将上线', icon: 'none' })
},
[ ] Step 3: Add styles for 五行沙盘
/* ===== 五行能量沙盘 ===== */
.wuxing-sandbox {
margin: 20rpx 30rpx;
padding: 30rpx;
background: linear-gradient(135deg, #1a1a2e, #16213e);
border-radius: 24rpx;
position: relative;
overflow: hidden;
}
.sandbox-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.sandbox-title {
font-size: 28rpx;
font-weight: bold;
color: #FFD700;
}
.sandbox-badge {
font-size: 22rpx;
color: #aaa;
background: rgba(255,255,255,0.1);
padding: 6rpx 16rpx;
border-radius: 20rpx;
}
.sandbox-star {
position: relative;
height: 400rpx;
display: flex;
justify-content: center;
align-items: center;
}
.star-point {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
width: 120rpx;
}
.star-point-top {
top: 0;
left: 50%;
transform: translateX(-50%);
}
.star-point-left {
top: 120rpx;
left: 20rpx;
}
.star-point-right {
top: 120rpx;
right: 20rpx;
}
.star-point-bl {
bottom: 40rpx;
left: 60rpx;
}
.star-point-br {
bottom: 40rpx;
right: 60rpx;
}
.point-icon {
font-size: 50rpx;
margin-bottom: 6rpx;
}
.point-label {
font-size: 22rpx;
color: #fff;
font-weight: bold;
}
.point-status {
font-size: 18rpx;
margin-top: 4rpx;
}
.point-status.inactive {
color: #666;
}
.point-status.active {
color: #4CAF50;
}
.star-center {
width: 120rpx;
height: 120rpx;
background: radial-gradient(circle, rgba(255,215,0,0.2), rgba(255,215,0,0.05));
border: 2rpx solid rgba(255,215,0,0.3);
border-radius: 50%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.center-text {
font-size: 22rpx;
color: #FFD700;
font-weight: bold;
}
.center-sub {
font-size: 18rpx;
color: #aaa;
}
.shenke-hint {
text-align: center;
margin: 20rpx 0;
}
.hint-text {
font-size: 24rpx;
color: #FFD700;
opacity: 0.8;
}
.dimension-bar {
display: flex;
justify-content: center;
align-items: center;
gap: 8rpx;
}
.dim-item {
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
}
.dim-item.inactive {
background: rgba(255,255,255,0.1);
color: #666;
}
.dim-item.active {
background: rgba(76,175,80,0.3);
color: #4CAF50;
}
.dim-arrow {
color: #444;
font-size: 20rpx;
}
/* ===== 会员Banner ===== */
.membership-banner {
margin: 20rpx 30rpx;
padding: 30rpx;
background: linear-gradient(135deg, #FFD700, #FFA500);
border-radius: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(255, 165, 0, 0.3);
}
.banner-content {
display: flex;
align-items: center;
}
.banner-icon {
font-size: 60rpx;
margin-right: 20rpx;
}
.banner-text {
flex: 1;
}
.banner-title {
font-size: 30rpx;
font-weight: bold;
color: #8B4513;
display: block;
}
.banner-subtitle {
font-size: 24rpx;
color: #A0522D;
margin-top: 6rpx;
display: block;
}
.banner-arrow {
font-size: 40rpx;
color: #8B4513;
}
[ ] Step 4: Same change in child-index.vue
Read the file and replicate the same 五行沙盘 + membership banner insertion in the same position. The child version can simplify or remove the membership banner (children don't buy memberships), but keep the 五行沙盘 with child-appropriate guidance text.
Files:
zxyj-web/src/views/admin/VendorReview.vuezxyj-web/src/router/index.jsCreate: zxyj-web/src/api/vendor.js
[ ] Step 1: Create vendor API file
// zxyj-web/src/api/vendor.js
import request from '@/utils/request'
// 服务商入驻申请列表
export function getVendorList(params) {
return request({
url: '/api/admin/vendor/list',
method: 'post',
data: params
})
}
// 审核服务商入驻
export function reviewVendor(data) {
return request({
url: '/api/admin/vendor/review',
method: 'post',
data: data
})
}
Check zxyj-web/src/utils/request.js to confirm the request wrapper pattern matches.
[ ] Step 2: Create VendorReview.vue
<template>
<div class="vendor-review">
<div class="header">
<h2>服务商入驻审核</h2>
<el-select v-model="statusFilter" placeholder="审核状态" @change="loadList" style="width: 150px;">
<el-option label="全部" value="" />
<el-option label="待审核" value="pending" />
<el-option label="已通过" value="approved" />
<el-option label="已拒绝" value="rejected" />
</el-select>
</div>
<el-table :data="list" v-loading="loading" border stripe>
<el-table-column prop="id" label="用户ID" width="80" />
<el-table-column prop="nickname" label="昵称" width="120" />
<el-table-column prop="realName" label="真实姓名" width="120" />
<el-table-column prop="phone" label="手机号" width="140" />
<el-table-column label="服务商类型" width="140">
<template slot-scope="{ row }">
{{ vendorTypeLabel(row.vendorType) }}
</template>
</el-table-column>
<el-table-column prop="vendorInfo" label="资质说明" min-width="200" show-overflow-tooltip />
<el-table-column label="审核状态" width="100">
<template slot-scope="{ row }">
<el-tag :type="statusType(row.vendorStatus)">{{ statusLabel(row.vendorStatus) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="vendorRejectReason" label="拒绝原因" width="150" show-overflow-tooltip />
<el-table-column label="操作" width="200" fixed="right">
<template slot-scope="{ row }">
<el-button v-if="row.vendorStatus === 'pending'" type="success" size="small" @click="handleReview(row, 'approve')">通过</el-button>
<el-button v-if="row.vendorStatus === 'pending'" type="danger" size="small" @click="showRejectDialog(row)">拒绝</el-button>
<span v-else class="no-op">—</span>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="onPageChange"
@current-change="onPageChange"
:current-page="page"
:page-size="size"
:total="total"
layout="total, prev, pager, next"
style="margin-top: 20px; text-align: right;"
/>
<!-- 拒绝弹窗 -->
<el-dialog title="填写拒绝原因" :visible.sync="rejectDialogVisible" width="400px">
<el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入拒绝原因" />
<span slot="footer">
<el-button @click="rejectDialogVisible = false">取消</el-button>
<el-button type="danger" @click="confirmReject">确认拒绝</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { getVendorList, reviewVendor } from '@/api/vendor'
export default {
data() {
return {
list: [],
loading: false,
page: 1,
size: 20,
total: 0,
statusFilter: 'pending',
rejectDialogVisible: false,
rejectTarget: null,
rejectReason: ''
}
},
mounted() {
this.loadList()
},
methods: {
vendorTypeLabel(type) {
const map = { planner: '成长规划师', activity_provider: '活动提供商', product_supplier: '商品供应商' }
return map[type] || type
},
statusType(status) {
const map = { pending: 'warning', approved: 'success', rejected: 'danger' }
return map[status] || 'info'
},
statusLabel(status) {
const map = { pending: '待审核', approved: '已通过', rejected: '已拒绝' }
return map[status] || status
},
async loadList() {
this.loading = true
try {
const res = await getVendorList({ page: this.page, size: this.size, status: this.statusFilter })
this.list = res.data.list || []
this.total = res.data.total || 0
} catch (e) {
this.$message.error('加载失败')
} finally {
this.loading = false
}
},
onPageChange(val) {
this.page = val
this.loadList()
},
async handleReview(row, action) {
try {
await reviewVendor({ userId: row.id, action })
this.$message.success('审核完成')
this.loadList()
} catch (e) {
this.$message.error('操作失败')
}
},
showRejectDialog(row) {
this.rejectTarget = row
this.rejectReason = ''
this.rejectDialogVisible = true
},
async confirmReject() {
if (!this.rejectReason) {
this.$message.warning('请填写拒绝原因')
return
}
await this.handleReview(this.rejectTarget, 'reject')
this.rejectDialogVisible = false
}
}
}
</script>
<style scoped>
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.no-op {
color: #ccc;
}
</style>
[ ] Step 3: Add route in router/index.js
Add the import and route at the end of the children array (before the closing ]):
{
path: 'vendor-review',
name: 'VendorReview',
component: () => import('@/views/admin/VendorReview.vue'),
meta: { title: '服务商审核' }
},
Add 'VendorReview' to the adminRoutes array in the route guard section:
'ServiceTypes', 'ServiceContents', 'PackageTemplates',
'AssessmentAdmin', 'VendorReview'
Files:
Modify: zxyj-web/src/views/Layout.vue
[ ] Step 1: Add sidebar menu item
Read Layout.vue first. Find the admin menu section (likely near other admin menu items like 服务类型管理). Add:
<el-menu-item index="/vendor-review">
<i class="el-icon-s-check"></i>
<span slot="title">服务商审核</span>
</el-menu-item>
Verify the existing pattern for menu items by reading the file first.
Run this after completing all tasks:
Spec coverage: Does Phase 0 deliver role model expansion (vendorType/vendorStatus/isFamilyAdmin), TabBar change, vendor apply/status/info endpoints, discover/shop placeholders, profile updates, membership banner, Web admin review? ✅ Yes — Tasks 1-14 cover all.
Placeholder scan: No "TBD", "TODO", "implement later" in code blocks. All code is complete.
Type consistency: vendorType (String), vendorStatus (String), isFamilyAdmin (Boolean) — consistent across all tasks. VendorService.apply returns Result. VendorController endpoints use @PostMapping. All consistent.
Dependencies: Tasks 1-2 must precede Task 3 (User entity + DB migration before VendorService). Task 5 (pages.json) must precede Tasks 7-8 (pages must be declared before they can be navigated to). Tasks 9-10 (vendor pages) depend on Task 4 (API calls). Task 11 (profile) depends on Task 4. Task 14 (sidebar) depends on Task 13.