|
|
@@ -0,0 +1,1468 @@
|
|
|
+# Phase 0: 骨架 — 角色模型 + TabBar + 服务商入驻
|
|
|
+
|
|
|
+> **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:**
|
|
|
+- All entities use `java.util.Date` and `implements Serializable`
|
|
|
+- All controllers use `@RequestMapping("/api/...")` + `@PostMapping`
|
|
|
+- DI via `@Resource`
|
|
|
+- No indexes, no constraints, no custom SQL (DDL in DatabaseInitializer only)
|
|
|
+- Vue 2 Options API, uni-app tags, rpx units, NO optional chaining `?.`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: User entity — add vendor/service provider fields
|
|
|
+
|
|
|
+**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`):
|
|
|
+
|
|
|
+```java
|
|
|
+ // 服务商类型: null=普通用户 / planner / activity_provider / product_supplier
|
|
|
+ private String vendorType;
|
|
|
+
|
|
|
+ // 服务商审核状态: null=未申请 / pending / approved / rejected
|
|
|
+ private String vendorStatus;
|
|
|
+
|
|
|
+ // 服务商审核拒绝原因
|
|
|
+ private String vendorRejectReason;
|
|
|
+
|
|
|
+ // 家庭管理员标记
|
|
|
+ private Boolean isFamilyAdmin;
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Verify compilation**
|
|
|
+
|
|
|
+Run: `cd zxyj-backend && mvn clean compile -q`
|
|
|
+Expected: BUILD SUCCESS
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: DatabaseInitializer — add vendor columns migration
|
|
|
+
|
|
|
+**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:
|
|
|
+
|
|
|
+```java
|
|
|
+// 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.
|
|
|
+
|
|
|
+- [ ] **Step 2: Add teacher→vendor data migration**
|
|
|
+
|
|
|
+At the end of `runMigrations()`, add data migration for existing teachers:
|
|
|
+
|
|
|
+```java
|
|
|
+// Phase 0: 迁移已有规划师到服务商体系
|
|
|
+jdbcTemplate.update(
|
|
|
+ "UPDATE users SET vendor_type = 'planner', vendor_status = 'approved' WHERE teacher_status = 'approved' AND vendor_type IS NULL"
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Verify compilation**
|
|
|
+
|
|
|
+Run: `cd zxyj-backend && mvn clean compile -q`
|
|
|
+Expected: BUILD SUCCESS
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: VendorController + VendorService (backend)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `zxyj-backend/src/main/java/com/zxyj/controller/vendor/VendorController.java`
|
|
|
+- Create: `zxyj-backend/src/main/java/com/zxyj/service/VendorService.java`
|
|
|
+- Modify: `zxyj-backend/src/main/java/com/zxyj/mapper/UserMapper.java` (only if methods are missing, likely already has basic CRUD)
|
|
|
+
|
|
|
+- [ ] **Step 1: Create VendorService**
|
|
|
+
|
|
|
+```java
|
|
|
+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**
|
|
|
+
|
|
|
+```java
|
|
|
+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.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: Admin vendor API — Web端审核服务商入驻
|
|
|
+
|
|
|
+**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:
|
|
|
+
|
|
|
+```java
|
|
|
+/**
|
|
|
+ * 服务商入驻申请列表
|
|
|
+ * 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:
|
|
|
+```java
|
|
|
+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.
|
|
|
+
|
|
|
+- [ ] **Step 2: Verify compilation**
|
|
|
+
|
|
|
+Run: `cd zxyj-backend && mvn clean compile -q`
|
|
|
+Expected: BUILD SUCCESS
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: Frontend API additions — vendor module
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `zxyj-frontend/utils/api.js`
|
|
|
+
|
|
|
+- [ ] **Step 1: Add vendor API calls to api.js**
|
|
|
+
|
|
|
+Find the end of file. Add:
|
|
|
+
|
|
|
+```javascript
|
|
|
+// ===================== 溪福俱乐部 - 服务商模块 =====================
|
|
|
+
|
|
|
+// 提交服务商入驻申请
|
|
|
+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', {})
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: TabBar change — pages.json
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `zxyj-frontend/pages.json`
|
|
|
+
|
|
|
+- [ ] **Step 1: Change TabBar list and add new page registrations**
|
|
|
+
|
|
|
+Replace the `tabBar.list` with:
|
|
|
+
|
|
|
+```json
|
|
|
+"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:
|
|
|
+
|
|
|
+```json
|
|
|
+ // ===== 溪福俱乐部 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": "服务商中心"
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 7: Discover placeholder page
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `zxyj-frontend/pages/discover/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: Create discover placeholder page**
|
|
|
+
|
|
|
+```vue
|
|
|
+<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>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 8: Shop placeholder page
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `zxyj-frontend/pages/shop/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: Create shop placeholder page**
|
|
|
+
|
|
|
+```vue
|
|
|
+<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>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 9: Vendor apply page (frontend)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `zxyj-frontend/pages/vendor/apply.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: Create vendor application form page**
|
|
|
+
|
|
|
+```vue
|
|
|
+<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>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 10: Vendor center page (frontend)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `zxyj-frontend/pages/vendor/center.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: Create vendor center page**
|
|
|
+
|
|
|
+```vue
|
|
|
+<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>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 11: Profile page — add membership + vendor entries
|
|
|
+
|
|
|
+**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:
|
|
|
+
|
|
|
+```vue
|
|
|
+ <!-- 会员中心(溪艾福) -->
|
|
|
+ <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>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Add `isVendor` data property and handler methods**
|
|
|
+
|
|
|
+In the `data()` function, add:
|
|
|
+```javascript
|
|
|
+isVendor: false,
|
|
|
+```
|
|
|
+
|
|
|
+In the `methods` section, add:
|
|
|
+```javascript
|
|
|
+ goToMembership() {
|
|
|
+ uni.showToast({ title: '即将上线', icon: 'none' })
|
|
|
+ },
|
|
|
+ goToVendorCenter() {
|
|
|
+ uni.navigateTo({ url: '/pages/vendor/center' })
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Load vendor status on page show**
|
|
|
+
|
|
|
+In the `onShow()` lifecycle hook, add vendor status check:
|
|
|
+```javascript
|
|
|
+ // 检查服务商状态
|
|
|
+ this.checkVendorStatus()
|
|
|
+```
|
|
|
+
|
|
|
+Add the method:
|
|
|
+```javascript
|
|
|
+ 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:
|
|
|
+```javascript
|
|
|
+import { vendorStatus } from '../../utils/api'
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 12: Homepage 五行能量沙盘(五角星)+ membership banner
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `zxyj-frontend/pages/index/parent-index.vue`
|
|
|
+- Modify: `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:
|
|
|
+
|
|
|
+```vue
|
|
|
+ <!-- 五行能量沙盘(溪艾福) -->
|
|
|
+ <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.
|
|
|
+
|
|
|
+- [ ] **Step 2: Add data and methods**
|
|
|
+
|
|
|
+In `data()`:
|
|
|
+```javascript
|
|
|
+membershipText: '开通家庭会员 畅享全部权益',
|
|
|
+```
|
|
|
+
|
|
|
+In `methods()`:
|
|
|
+```javascript
|
|
|
+goToDomain(domain) {
|
|
|
+ uni.showToast({ title: '即将上线', icon: 'none' })
|
|
|
+},
|
|
|
+goToMembership() {
|
|
|
+ uni.showToast({ title: '即将上线', icon: 'none' })
|
|
|
+},
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Add styles for 五行沙盘**
|
|
|
+
|
|
|
+```css
|
|
|
+/* ===== 五行能量沙盘 ===== */
|
|
|
+.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.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 13: Web admin — vendor review page
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `zxyj-web/src/views/admin/VendorReview.vue`
|
|
|
+- Modify: `zxyj-web/src/router/index.js`
|
|
|
+- Create: `zxyj-web/src/api/vendor.js`
|
|
|
+
|
|
|
+- [ ] **Step 1: Create vendor API file**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 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**
|
|
|
+
|
|
|
+```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 `]`):
|
|
|
+
|
|
|
+```javascript
|
|
|
+ {
|
|
|
+ path: 'vendor-review',
|
|
|
+ name: 'VendorReview',
|
|
|
+ component: () => import('@/views/admin/VendorReview.vue'),
|
|
|
+ meta: { title: '服务商审核' }
|
|
|
+ },
|
|
|
+```
|
|
|
+
|
|
|
+Add `'VendorReview'` to the `adminRoutes` array in the route guard section:
|
|
|
+```javascript
|
|
|
+ 'ServiceTypes', 'ServiceContents', 'PackageTemplates',
|
|
|
+ 'AssessmentAdmin', 'VendorReview'
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 14: Web admin — add sidebar menu for vendor review
|
|
|
+
|
|
|
+**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:
|
|
|
+
|
|
|
+```html
|
|
|
+<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.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Self-review
|
|
|
+
|
|
|
+Run this after completing all tasks:
|
|
|
+
|
|
|
+1. **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.
|
|
|
+
|
|
|
+2. **Placeholder scan:** No "TBD", "TODO", "implement later" in code blocks. All code is complete.
|
|
|
+
|
|
|
+3. **Type consistency:** `vendorType` (String), `vendorStatus` (String), `isFamilyAdmin` (Boolean) — consistent across all tasks. `VendorService.apply` returns `Result`. `VendorController` endpoints use `@PostMapping`. All consistent.
|
|
|
+
|
|
|
+4. **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.
|