feat(admin): 后台登录页品牌化 + 26 个业务模块与前端对齐 + 数据同步打通

一、登录页重构(暖色调品牌版)
- 左右分栏卡片:44% 品牌面板(花红渐变 + 光斑 + 内联 SVG 花瓣 Logo + 花期字标/口号)
  + 56% 表单面板(52px 输入框、内嵌矢量图标、聚焦暖色光晕、渐变主按钮)
- 与前台同一套设计令牌(#E8604C / #F58C7C / #FFF9F5 / #3E2C27),响应式适配
- 完整保留 #login-form/#pd-form-username/#profile-img/#errtips/token/验证码/保持登录等结构,后台登录 JS 零改动

二、后台业务模块(26 个,与前端 M1–M9 一一对应)
- 新增通用 Manage 控制器(列表/搜索/排序/新增/编辑/软删除/批量/列元数据)
- 新增 ManageConfig(26 张表的字段标签/类型/枚举/搜索配置)与 FormBuilder(表单控件渲染)
- 新增通用视图 manage/{index,add,edit,style}.html + public/assets/js/backend/manage.js
- 新增 26 个薄控制器(各 8 行,仅声明 )
- db/05_admin_menu.sql:花期运营菜单树 + 6 个通用操作权限,含站点名品牌化

三、品牌一致性
- site.php 站点名改为「花期运营中心」,顶栏 Logo 增加矢量花瓣
- 业务页暖色表头、渐变按钮、圆角输入、品牌色开关

四、验证(全绿)
- 后台自检 31 项:登录页品牌、框架菜单、26 模块页面 200 + 列表数据、后台编辑写库、
  后台改动 → 前台 API 同步读取、数据还原
- 前台接口 82 项断言、契约 84 接口、Vue 20/20、SCSS 19/19、无 emoji 均通过
- 修复 5 个真实问题:方法签名不兼容致 500、菜单缓存、SQL 字符集乱码、
  snake_case URL 与权限规则命名、会话文件属主导致的自检 500
这个提交包含在:
linda
2026-11-06 11:10:19 +08:00
父节点 ea2be7af8c
当前提交 56f5f60aa5
共有 42 个文件被更改,包括 2415 次插入 和 116 次删除

7
.gitignore vendored
查看文件

@@ -15,7 +15,12 @@ dist/
server/vendor/
server/thinkphp/
server/runtime/
server/public/assets/
server/public/assets/*
!server/public/assets/js/
server/public/assets/js/*
!server/public/assets/js/backend/
server/public/assets/js/backend/*
!server/public/assets/js/backend/manage.js
server/public/uploads/
server/node_modules/
server/.env

查看文件

@@ -0,0 +1,135 @@
# 花期 APP · 运营后台(与前端模块对齐)与数据同步
> 本轮内容:**运营后台登录页品牌化重构** + **后台 26 个业务模块与前端 M1–M9 对齐** + **前后端数据同步全链路验证**。
> 关联:[02-开发文档](./02-开发文档.md)、[05-功能覆盖度与实现说明](./05-功能覆盖度与实现说明.md)
---
## 1. 后台登录页重构(暖色调 · 品牌化)
| 设计项 | 实现 |
|---|---|
| 布局 | 左侧 44% 品牌面板(渐变花红 + 光斑)+ 右侧表单面板,940px 居中卡片,圆角 28px |
| 品牌 | 内联 SVG 花瓣 Logo、`花期` 字标(字距 8px)、`温柔记录 · 静待花开`,底部「运营管理后台 / 内容审核 · 用户运营 · 数据看板」 |
| 色彩 | 与前台同一套令牌:`#E8604C` 主色、`#F58C7C` 渐变、`#FFF9F5` 暖米白、`#3E2C27` 暖棕黑 |
| 表单 | 52px 高输入框、14px 圆角、内嵌矢量图标、聚焦暖色光晕;验证码内嵌右侧、可点击刷新 |
| 交互 | 渐变主按钮(投影 + hover 提亮 + 按下位移);错误提示条(暖红底) |
| 响应式 | ≤860px 隐藏品牌面板,≤480px 全屏铺满 |
| 兼容 | 保留 `#login-form` / `#pd-form-username` / `#profile-img` / `#profile-name` / `#errtips` / `{:token()}` / 验证码 / 保持登录等结构,后台原有登录 JS 无需改动 |
后台框架同步品牌化:`application/extra/site.php` 站点名改为「花期运营中心」,顶栏 Logo 增加矢量花瓣图标;业务页统一使用暖色表头、渐变主按钮、圆角输入框与品牌色状态开关。
---
## 2. 后台业务模块矩阵(26 个,与前端一一对应)
| 前端模块 | 后台菜单 | 数据表 | 权限节点 |
|---|---|---|---|
| M1 周期管理 | 周期档案 / 每日症状记录 / 症状标签 / 智能提醒 | `fa_cycle`、`fa_cycle_log`、`fa_symptom_tag`、`fa_reminder` | `cycle/index`、`cyclelog/index`、`symptomtag/index`、`reminder/index` |
| M2 情绪疗愈 | 情绪日记 | `fa_mood_log` | `moodlog/index` |
| 内容中心(M2/M5 共用) | 内容库 / 内容分类 | `fa_content`、`fa_content_category` | `content/index`、`contentcategory/index` |
| M3 科学备孕 | 基础体温 / 试纸识别 / 任务管理 | `fa_bbt_record`、`fa_lh_test`、`fa_task` | `bbtrecord/index`、`lhtest/index`、`task/index` |
| M4 家人共护 | 家人绑定 / 沟通留言 / 爱心积分 | `fa_family_bind`、`fa_care_message`、`fa_point_log` | `familybind/index`、`caremessage/index`、`pointlog/index` |
| M7 两性关系 | 纪念日 / 课程订单 | `fa_memory`、`fa_course_order` | `memory/index`、`courseorder/index` |
| M8 亲子教育 | 宝宝档案 / 成长记录 / 疫苗与儿保 | `fa_baby`、`fa_growth_record`、`fa_vaccine` | `baby/index`、`growthrecord/index`、`vaccine/index` |
| M9 心理服务 | 咨询师 / 咨询订单 / 量表管理 / 测评记录 | `fa_counselor`、`fa_consult_order`、`fa_assessment`、`fa_assessment_record` | `counselor/index`、`consultorder/index`、`assessment/index`、`assessmentrecord/index` |
| M6 会员中心 | 会员套餐 / 会员订单 / 隐私与提醒 / 数据备份 | `fa_member_plan`、`fa_member_order`、`fa_user_setting`、`fa_user_backup` | `memberplan/index`、`memberorder/index`、`usersetting/index`、`userbackup/index` |
| 用户(FastAdmin 原生) | 会员管理 | `fa_user` | `user/user` |
菜单脚本:`server/db/05_admin_menu.sql`(可重复执行),父菜单「花期运营」+ 26 个业务菜单 + 6 个通用操作权限(`manage/index|add|edit|del|multi|columns`)。
---
## 3. 通用 CRUD 架构(一套代码覆盖 26 张表)
```
application/admin/
├── controller/Manage.php # 通用列表/新增/编辑/删除/批量/列定义
├── controller/{Cycle,Content,…}.php # 26 个 8 行薄控制器(只声明 $fixedTable)
├── library/ManageConfig.php # 表注册表:字段标签/类型/枚举/搜索/只读
├── library/FormBuilder.php # 按字段类型渲染表单控件(文本/下拉/日期/开关/用户)
└── view/manage/{index,add,edit,style}.html # 通用列表与表单视图(暖色主题)
public/assets/js/backend/manage.js # 通用列表脚本:按后端列元数据动态构建表格
```
**能力**
- 列表:分页、排序、关键词模糊搜索(按配置的 searchable 字段)、`deletetime` 软删除隔离
- 表单:按字段类型自动渲染,`select` 走统一枚举表(状态/审核/权限/支付/咨询状态等),`user` 字段自动带出「昵称 (#ID)」下拉
- 校验与转换:按列类型强转(int/decimal/日期/时间戳/布尔),只收集白名单字段,避免脏字段写入
- 删除:有 `deletetime` 的表走软删除(支持恢复),只读表(如用户)禁止写入
- 权限:26 个模块各自独立权限节点,可对子管理员单独授权
**新增一个业务模块只需要**:
1. `ManageConfig::all()` 里加一条表配置(字段中文名 + 类型 + 枚举)
2. 新建一个 8 行控制器 `class Xxx extends Manage { protected $fixedTable = 'fa_xxx'; }`
3. `db/05_admin_menu.sql` 追加菜单(或直接在「菜单规则」后台添加)
---
## 4. 关键实现细节(踩坑记录)
| 问题 | 原因 | 解决 |
|---|---|---|
| 模块页全部 500 | `Manage::edit()/del()/multi()` 与 `app\admin\library\traits\Backend` 的 `($ids = null)` 签名不兼容,PHP 8 直接 Fatal | 统一为 `($ids = null)` |
| 菜单不显示 | 后台配置/规则缓存(`runtime/cache`) | 清 `runtime/cache`、`runtime/temp` 后正常 |
| 菜单标题乱码 | 导入 SQL 未声明客户端字符集 | SQL 顶部加 `SET NAMES utf8mb4;` |
| 菜单点击 404 | ThinkPHP 需**下划线**形式的 URL 才能映射到驼峰类(`user_backup` → `UserBackup`),而权限规则名是**小写无下划线**(`userbackup/index`) | 菜单 `url` 用 snake_case,规则 `name` 用 lowercase |
| 自检脚本 500 | 构造的会话文件属主为 root,FPM 为 www,`session_start(O_RDWR)` 权限失败 | 会话文件 `chown www:www && chmod 0660` |
---
## 5. 前后端数据同步验证(31 项,全部通过)
脚本:`scripts/verify/admin_backend_check.php`(在服务器上执行)
```bash
php scripts/verify/admin_backend_check.php /www/wwwroot/huaqiapp.gzyakj.com.cn huaqiapp.gzyakj.com.cn
```
覆盖:
1. **登录页品牌**:HTTP 200 且包含「登录运营后台」「花期」
2. **后台框架 + 菜单**:HTTP 200 且包含「花期运营」「周期档案」
3. **26 个业务模块**:页面 200(含 `hq-admin` 标记)+ 列表接口返回 `total`(实测数据量:症状标签 20、内容 70、分类 17、任务 12、疫苗 17、测评记录 8…)
4. **写入链路**:后台 AJAX 编辑 → 断言返回 `code=1` → 断言数据库字段已更新
5. **同步链路**:会员 token 调 `/api/content/detail` → 断言前台接口读到后台新标题
6. **还原**:改回原值并断言一致(不污染线上数据)
实测输出(节选):
```
✅ 登录页(暖色调品牌版)
✅ 后台框架 + 花期运营菜单
✅ 内容库 页面200 · 数据 70 条
✅ 咨询师 页面200 · 数据 3 条
✅ 后台编辑写入数据库
✅ 后台改动 → 前台 API 同步(读取到:同步自检110816)
✅ 数据还原(经期温热暖身餐)
========================================
通过 31 项,失败 0 项
```
**其它回归**:前台接口 82 项断言全绿;前后端契约 84 接口 0 偏差;Vue 20/20、SCSS 19/19、无 emoji 校验通过。
---
## 6. 运维与回滚
| 项 | 路径 |
|---|---|
| 后台改动前备份 | `/www/backup/huaqiapp_admin_<时间戳>/`(controller、view/manage、login.html、header.html、site.php、fa_auth_rule 导出) |
| 菜单重建 | `mysql < db/05_admin_menu.sql` |
| 清缓存 | `rm -rf runtime/cache/* runtime/temp/*` |
| 回滚登录页 | 从备份目录覆盖 `application/admin/view/index/login.html` |
---
## 7. 已知限制 / 后续
| # | 限制 | 说明 |
|---|---|---|
| 1 | 权限粒度 | 26 个模块独立授权已可用;但「通用 CRUD」的 add/edit/del 共用 `manage/*` 节点,若需按模块细分写权限,可为每个模块补充 `{controller}/add` 等规则 |
| 2 | 回收站 | 软删除已生效,但未接入 FastAdmin 回收站 UI(需为每个模块绑定模型) |
| 3 | 导出 | 通用列表暂未开启 Excel 导出(PHP 8.5 缺 `zip` 扩展,需先修复扩展) |
| 4 | 富文本 | 内容正文当前为纯文本域;接富文本编辑器(如 summernote)可提升编辑体验 |
| 5 | 图片上传 | 内容封面/试纸图片目前为 URL 文本;可接入「附件管理」选择器 |

查看文件

@@ -7,6 +7,7 @@
| [03-数据模型与接口设计](./03-数据模型与接口设计.md) | 表结构草案(DDL)、接口清单、业务规则、落地顺序 | 后端、前端 |
| [04-前端设计系统与联调验证](./04-前端设计系统与联调验证.md) | 暖色调设计令牌、组件规范、页面地图、接口契约表、全链路验证报告 | 设计、前端、测试 |
| [05-功能覆盖度与实现说明](./05-功能覆盖度与实现说明.md) | M1–M9 共 47 个功能点逐条对照、接口与表映射、验证结论、已知限制 | 产品、开发、测试、运营 |
| [06-运营后台与数据同步](./06-运营后台与数据同步.md) | 后台登录页品牌化、26 个业务模块与前端对齐、通用 CRUD 架构、数据同步验证、运维回滚 | 运营、开发、运维 |
## 快速了解
@@ -15,6 +16,7 @@
- **仓库**:前端在根目录,后端源码在 `server/`(与线上服务器同步)
- **上线节奏**:V1.0(P0,19 项)→ V1.1(P1,18 项)→ V2.0(P2,10 项)
- **当前进度**:M1–M9 共 **47 个功能点全部实现并联调通过**(详见 05 文档)
- **运营后台**:登录页品牌化 + 26 个业务模块(与前端模块一一对应)+ 通用 CRUD(详见 06 文档)
- **变现**:会员订阅为主轴(月/年/终身),课程与咨询/测评为副,按次增值补充;坚持零广告
- **设计**:暖色调(花红 `#E8604C` + 蜜橙 `#F5A65B` + 暖米白 `#FFF9F5`),设计令牌见 `uni.scss`
- **图标**:全站 61 个内联 SVG 矢量图标(`utils/icons.js` + `<hq-icon>`),**禁止使用 emoji 图片**
@@ -27,6 +29,9 @@ python3 scripts/verify/contract_check.py # 前后端契约静态校验
node scripts/verify/vue_compile_check.mjs # Vue SFC 编译(需 @vue/compiler-sfc)
node scripts/verify/scss_compile_check.mjs # SCSS 编译(需 sass)
python3 scripts/verify/no_emoji_check.py # 图标规范(禁止 emoji / 图标名与组件注册)
# 运营后台(在服务器执行)
php scripts/verify/admin_backend_check.php <站点目录> <站点域名> # 26 模块 + 数据同步 31 项
```
## 重要约定

查看文件

@@ -0,0 +1,174 @@
<?php
/**
* 花期 APP · 运营后台全链路自检(在服务器上执行)
*
* 用途:验证「运营后台 26 个业务模块 + 通用 CRUD + 前后端数据同步」
* 用法:
* php scripts/verify/admin_backend_check.php /www/wwwroot/huaqiapp.gzyakj.com.cn huaqiapp.gzyakj.com.cn
*
* 说明:
* - 通过读取 .env 获取数据库账号,构造一个管理员会话(避免依赖验证码)用于自检;
* - 仅做只读校验与一次「改标题→读前台接口→还原」的同步验证;
* - 不写入任何凭据到仓库。
*/
$site = $argv[1] ?? '';
$host = $argv[2] ?? '';
if (!$site || !$host) {
fwrite(STDERR, "用法: php admin_backend_check.php <站点目录> <站点域名>\n");
exit(2);
}
$env = parse_ini_file($site . '/.env', true);
$db = $env['database'] ?? [];
$pdo = new PDO(
'mysql:host=' . ($db['hostname'] ?? '127.0.0.1') . ';port=' . ($db['hostport'] ?? 3306) . ';dbname=' . $db['database'] . ';charset=utf8mb4',
$db['username'] ?? '',
$db['password'] ?? ''
);
$prefix = $db['prefix'] ?? 'fa_';
// 读取 token.key(用于构造管理员 safecode)
$config = file_get_contents($site . '/application/config.php');
preg_match("/'key'\s*=>\s*'([^']+)'/", $config, $m);
$tokenKey = $m[1] ?? '';
$admin = $pdo->query("SELECT * FROM {$prefix}admin WHERE id=1")->fetch(PDO::FETCH_ASSOC);
$sid = bin2hex(random_bytes(16));
$safecode = md5(md5($admin['username']) . md5(substr($admin['password'], 0, 6)) . $tokenKey);
$admin['safecode'] = $safecode;
$admin['loginip'] = '127.0.0.1';
$admin['logintime'] = time();
session_id($sid);
session_start();
$_SESSION['think'] = ['admin' => $admin];
session_write_close();
$sessPath = session_save_path() ?: sys_get_temp_dir();
$sessFile = rtrim($sessPath, '/') . '/sess_' . $sid;
// Web 进程(www)需要可读写该会话文件,否则 session_start 会因 O_RDWR 失败
@chown($sessFile, 'www');
@chgrp($sessFile, 'www');
@chmod($sessFile, 0660);
$base = 'http://127.0.0.1/wechLIGOut.php';
$cookie = 'PHPSESSID=' . $sid;
// 前台 API 基址(用于数据同步验证)
$apiBase = 'http://127.0.0.1';
function http_get($url, $cookie, $ajax = false)
{
$opts = ['http' => ['method' => 'GET', 'header' => "Host: {$GLOBALS['host']}\r\nCookie: {$cookie}\r\n" . ($ajax ? "X-Requested-With: XMLHttpRequest\r\n" : ''), 'timeout' => 20, 'ignore_errors' => true]];
$res = @file_get_contents($url, false, stream_context_create($opts));
$status = 0;
foreach ($http_response_header ?? [] as $line) {
if (preg_match('#HTTP/\S+\s+(\d{3})#', $line, $mm)) {
$status = (int)$mm[1];
}
}
return [$status, (string)$res];
}
function http_post($url, $cookie, $data)
{
$body = http_build_query($data);
$opts = ['http' => ['method' => 'POST', 'header' => "Host: {$GLOBALS['host']}\r\nCookie: {$cookie}\r\nX-Requested-With: XMLHttpRequest\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: " . strlen($body) . "\r\n", 'content' => $body, 'timeout' => 20, 'ignore_errors' => true]];
$res = @file_get_contents($url, false, stream_context_create($opts));
return json_decode((string)$res, true) ?: ['code' => 0, 'msg' => substr((string)$res, 0, 120)];
}
$modules = [
'cycle' => '周期档案', 'cycle_log' => '每日症状记录', 'symptom_tag' => '症状标签', 'reminder' => '智能提醒',
'mood_log' => '情绪日记', 'content' => '内容库', 'content_category' => '内容分类',
'bbt_record' => '基础体温', 'lh_test' => '试纸识别', 'task' => '任务管理',
'family_bind' => '家人绑定', 'care_message' => '沟通留言', 'point_log' => '爱心积分',
'memory' => '纪念日', 'course_order' => '课程订单',
'baby' => '宝宝档案', 'growth_record' => '成长记录', 'vaccine' => '疫苗与儿保',
'counselor' => '咨询师', 'consult_order' => '咨询订单', 'assessment' => '量表管理', 'assessment_record' => '测评记录',
'member_plan' => '会员套餐', 'member_order' => '会员订单', 'user_setting' => '隐私与提醒', 'user_backup' => '数据备份',
];
$pass = 0;
$fail = [];
// 1. 登录页品牌
list($code, $html) = http_get($base . '/index/login', '');
$ok = $code === 200 && strpos($html, '登录运营后台') !== false && strpos($html, '花期') !== false;
$ok ? $pass++ : $fail[] = '登录页品牌渲染';
echo ($ok ? ' ✅' : ' ❌') . " 登录页(暖色调品牌版)\n";
// 2. 品牌与菜单
list($code, $html) = http_get($base . '/index/index', $cookie);
$ok = $code === 200 && strpos($html, '花期运营') !== false && strpos($html, '周期档案') !== false;
$ok ? $pass++ : $fail[] = '后台框架与菜单';
echo ($ok ? ' ✅' : ' ❌') . " 后台框架 + 花期运营菜单\n";
// 3. 26 个模块页面与列表接口
foreach ($modules as $module => $title) {
list($code, $html) = http_get($base . '/' . $module . '/index', $cookie);
list($acode, $json) = http_get($base . '/' . $module . '/index?limit=2&offset=0', $cookie, true);
$data = json_decode($json, true);
$ok = $code === 200 && strpos($html, 'hq-admin') !== false && is_array($data) && isset($data['total']);
if ($ok) {
$pass++;
printf(" ✅ %-18s 页面200 · 数据 %s 条\n", $title, $data['total']);
} else {
$fail[] = $module;
printf(" ❌ %-18s http=%s ajax=%s\n", $title, $code, substr($json, 0, 80));
}
}
// 4. 数据同步:后台改内容标题 → 数据库 → 前台接口 → 还原
$row = $pdo->query("SELECT id,title FROM {$prefix}content WHERE type='advice' ORDER BY id LIMIT 1")->fetch(PDO::FETCH_ASSOC);
if ($row) {
$newTitle = '同步自检' . date('His');
// 4.1 后台编辑写库
$res = http_post($base . '/content/edit?ids=' . $row['id'], $cookie, ['title' => $newTitle]);
$dbTitle = $pdo->query("SELECT title FROM {$prefix}content WHERE id=" . (int)$row['id'])->fetchColumn();
$okWrite = (($res['code'] ?? 0) == 1) && $dbTitle === $newTitle;
$okWrite ? $pass++ : $fail[] = '后台编辑写库';
echo ($okWrite ? ' ✅' : ' ❌') . " 后台编辑写入数据库\n";
// 4.2 前台接口读取(会员 token)
$token = '';
$loginCtx = stream_context_create(['http' => [
'method' => 'POST',
'header' => "Host: {$host}\r\nContent-Type: application/x-www-form-urlencoded\r\n",
'content' => http_build_query(['account' => 'demo', 'password' => 'demo123456']),
'timeout' => 15, 'ignore_errors' => true,
]]);
$login = json_decode((string)@file_get_contents($apiBase . '/api/user/login', false, $loginCtx), true);
$token = $login['data']['userinfo']['token'] ?? '';
$apiTitle = '';
if ($token) {
$ctx = stream_context_create(['http' => [
'method' => 'GET',
'header' => "Host: {$host}\r\ntoken: {$token}\r\n",
'timeout' => 15, 'ignore_errors' => true,
]]);
$api = json_decode((string)@file_get_contents($apiBase . '/api/content/detail?id=' . $row['id'], false, $ctx), true);
$apiTitle = $api['data']['title'] ?? '';
}
$okSync = $apiTitle === $newTitle;
$okSync ? $pass++ : $fail[] = '前台接口同步';
echo ($okSync ? ' ✅' : ' ❌') . " 后台改动 → 前台 API 同步(读取到:{$apiTitle})\n";
// 4.3 还原
http_post($base . '/content/edit?ids=' . $row['id'], $cookie, ['title' => $row['title']]);
$restored = $pdo->query("SELECT title FROM {$prefix}content WHERE id=" . (int)$row['id'])->fetchColumn();
$okRestore = $restored === $row['title'];
$okRestore ? $pass++ : $fail[] = '内容还原';
echo ($okRestore ? ' ✅' : ' ❌') . " 数据还原({$restored})\n";
}
echo "=" . str_repeat('=', 60) . "\n";
echo "通过 {$pass} 项,失败 " . count($fail) . " 项\n";
@unlink($sessFile);
if ($fail) {
echo "失败项:" . implode('、', $fail) . "\n";
exit(1);
}
echo "🎉 运营后台全链路自检通过\n";

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 量表管理(心理服务)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_assessment
*/
class Assessment extends Manage
{
protected $fixedTable = 'fa_assessment';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 测评记录(心理服务)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_assessment_record
*/
class AssessmentRecord extends Manage
{
protected $fixedTable = 'fa_assessment_record';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 宝宝档案(亲子教育)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_baby
*/
class Baby extends Manage
{
protected $fixedTable = 'fa_baby';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 基础体温(科学备孕)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_bbt_record
*/
class BbtRecord extends Manage
{
protected $fixedTable = 'fa_bbt_record';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 沟通留言(家人共护)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_care_message
*/
class CareMessage extends Manage
{
protected $fixedTable = 'fa_care_message';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 咨询订单(心理服务)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_consult_order
*/
class ConsultOrder extends Manage
{
protected $fixedTable = 'fa_consult_order';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 内容库(内容中心)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_content
*/
class Content extends Manage
{
protected $fixedTable = 'fa_content';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 内容分类(内容中心)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_content_category
*/
class ContentCategory extends Manage
{
protected $fixedTable = 'fa_content_category';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 咨询师(心理服务)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_counselor
*/
class Counselor extends Manage
{
protected $fixedTable = 'fa_counselor';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 课程订单(两性关系)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_course_order
*/
class CourseOrder extends Manage
{
protected $fixedTable = 'fa_course_order';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 周期档案(周期管理)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_cycle
*/
class Cycle extends Manage
{
protected $fixedTable = 'fa_cycle';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 每日症状记录(周期管理)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_cycle_log
*/
class CycleLog extends Manage
{
protected $fixedTable = 'fa_cycle_log';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 家人绑定(家人共护)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_family_bind
*/
class FamilyBind extends Manage
{
protected $fixedTable = 'fa_family_bind';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 成长记录(亲子教育)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_growth_record
*/
class GrowthRecord extends Manage
{
protected $fixedTable = 'fa_growth_record';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 试纸识别(科学备孕)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_lh_test
*/
class LhTest extends Manage
{
protected $fixedTable = 'fa_lh_test';
}

查看文件

@@ -0,0 +1,503 @@
<?php
namespace app\admin\controller;
use app\admin\library\FormBuilder;
use app\admin\library\ManageConfig;
use app\common\controller\Backend;
use think\Db;
use think\exception\PDOException;
use think\exception\ValidateException;
/**
* 运营后台 · 通用业务数据管理
*
* 与前端 M1–M9 业务模块一一对应(见 ManageConfig),统一提供:
* 列表(分页/搜索/排序)、新增、编辑、删除(软删除优先)、批量设置。
*
* 访问方式:manage/index?table=fa_content
*/
class Manage extends Backend
{
/** @var string 子类可指定固定表名(各业务模块控制器使用) */
protected $fixedTable = '';
/** @var string 当前业务表 */
protected $table = '';
/** @var array 表配置 */
protected $config = [];
/** @var array 真实字段(含类型) */
protected $columns = [];
public function _initialize()
{
parent::_initialize();
$table = $this->fixedTable ?: $this->request->param('table', '');
$config = ManageConfig::get($table);
if (!$config) {
$this->error('模块不存在或未授权');
}
$this->table = $table;
$this->config = $config;
$this->columns = $this->describeTable($table);
$this->view->assign([
'table' => $table,
'tableTitle' => $config['title'],
'tableGroup' => $config['group'],
'readonlyTable' => !empty($config['readonly_table']),
]);
// 所有业务模块复用同一套前端脚本与视图
$this->assignconfig('controllername', 'manage');
$this->assignconfig('table', $table);
$this->assignconfig('columns', $this->columnMeta());
}
/* ============================================================
* 列表
* ============================================================ */
public function index()
{
if ($this->request->isAjax()) {
return json($this->getList());
}
return $this->view->fetch('manage/index');
}
/** 列定义(供前端 bootstrapTable 构建) */
public function columns()
{
return json(['code' => 1, 'data' => ['columns' => $this->columnMeta()]]);
}
/* ============================================================
* 新增 / 编辑
* ============================================================ */
public function add()
{
if ($this->request->isPost()) {
return $this->save();
}
$users = $this->userOptions();
$this->view->assign('row', []);
$this->view->assign('users', $users);
$this->view->assign('formHtml', FormBuilder::render($this->config['fields'], [], $users));
return $this->view->fetch('manage/add');
}
public function edit($ids = null)
{
$ids = $this->request->param('ids', '');
if ($this->request->isPost()) {
return $this->save($ids);
}
$row = Db::name($this->shortTable())->where('id', (int)$ids)->find();
if (!$row) {
$this->error('记录不存在');
}
$users = $this->userOptions();
$formRow = $this->formatRowForForm($row);
$this->view->assign('row', $formRow);
$this->view->assign('users', $users);
$this->view->assign('formHtml', FormBuilder::render($this->config['fields'], $formRow, $users));
return $this->view->fetch('manage/edit');
}
protected function save($ids = '')
{
if (!empty($this->config['readonly_table'])) {
$this->error('该模块仅支持查看');
}
$data = $this->collectPost();
if (!$data) {
$this->error('没有需要保存的字段');
}
$short = $this->shortTable();
try {
if ($ids) {
$exist = Db::name($short)->where('id', (int)$ids)->find();
if (!$exist) {
$this->error('记录不存在');
}
if (isset($this->columns['updatetime'])) {
$data['updatetime'] = time();
}
Db::name($short)->where('id', (int)$ids)->update($data);
$id = (int)$ids;
} else {
if (isset($this->columns['createtime'])) {
$data['createtime'] = time();
}
if (isset($this->columns['updatetime'])) {
$data['updatetime'] = time();
}
$id = Db::name($short)->insertGetId($data);
}
} catch (ValidateException $e) {
$this->error($e->getMessage());
} catch (PDOException $e) {
$this->error($e->getMessage());
}
$this->success($ids ? '修改成功' : '新增成功', null, ['id' => $id]);
}
/* ============================================================
* 删除 / 批量
* ============================================================ */
public function del($ids = null)
{
if (!empty($this->config['readonly_table'])) {
$this->error('该模块仅支持查看');
}
$ids = $this->parseIds($ids);
if (!$ids) {
$this->error('参数错误');
}
$short = $this->shortTable();
if (isset($this->columns['deletetime'])) {
Db::name($short)->whereIn('id', $ids)->update(['deletetime' => time()]);
// 支持恢复
if ($this->request->param('restore')) {
Db::name($short)->whereIn('id', $ids)->update(['deletetime' => null]);
}
} else {
Db::name($short)->whereIn('id', $ids)->delete();
}
$this->success('删除成功');
}
/** 批量设置字段(如状态、审核结果) */
public function multi($ids = null)
{
if (!empty($this->config['readonly_table'])) {
$this->error('该模块仅支持查看');
}
$ids = $this->parseIds($ids);
if (!$ids) {
$this->error('参数错误');
}
$params = $this->request->post();
$allow = [];
foreach ($params as $field => $value) {
if ($field === 'ids' || !isset($this->columns[$field])) {
continue;
}
if (!isset($this->config['fields'][$field])) {
continue;
}
$allow[$field] = $this->castValue($field, $value);
}
if (!$allow) {
$this->error('没有可设置的字段');
}
if (isset($this->columns['updatetime'])) {
$allow['updatetime'] = time();
}
Db::name($this->shortTable())->whereIn('id', $ids)->update($allow);
$this->success('批量更新成功');
}
/* ============================================================
* 内部方法
* ============================================================ */
protected function getList()
{
$short = $this->shortTable();
$query = Db::name($short);
// 过滤已删除
if (isset($this->columns['deletetime']) && !$this->request->param('with_deleted')) {
$query->whereNull('deletetime');
}
// 关键词搜索
$search = trim((string)$this->request->param('search', ''));
if ($search !== '') {
$like = [];
foreach ($this->config['fields'] as $field => $def) {
if (!empty($def['search']) && isset($this->columns[$field])) {
$like[] = [$field, 'like', '%' . $search . '%'];
}
}
if ($like) {
$query->where(function ($q) use ($like) {
foreach ($like as $index => $item) {
if ($index === 0) {
$q->where($item[0], $item[1], $item[2]);
} else {
$q->whereOr($item[0], $item[1], $item[2]);
}
}
});
}
}
// 精确过滤
$filter = $this->request->param('filter', '');
if ($filter) {
$filterArr = is_array($filter) ? $filter : json_decode(htmlspecialchars_decode($filter, ENT_QUOTES), true);
if (is_array($filterArr)) {
foreach ($filterArr as $field => $value) {
if ($value === '' || $value === null || !isset($this->columns[$field])) {
continue;
}
$query->where($field, $value);
}
}
}
$total = (clone $query)->count();
$limit = min(200, max(1, (int)$this->request->param('limit', 20)));
$offset = max(0, (int)$this->request->param('offset', 0));
$sort = $this->request->param('sort', '');
$order = strtolower($this->request->param('order', 'desc')) === 'asc' ? 'asc' : 'desc';
if ($sort && isset($this->columns[$sort])) {
$query->order($sort, $order);
} else {
$query->orderRaw($this->config['sort'] ?? 'id desc');
}
$rows = $query->limit($offset, $limit)->select();
$users = $this->userMap($rows);
$list = [];
foreach ($rows as $row) {
$list[] = $this->formatRow($row, $users);
}
return ['total' => $total, 'rows' => $list];
}
protected function columnMeta()
{
$meta = [['field' => 'id', 'title' => 'ID', 'sortable' => true, 'width' => 70]];
foreach ($this->config['fields'] as $field => $def) {
if (isset($def['form']) && $def['form'] === false) {
continue;
}
if (!isset($this->columns[$field])) {
continue;
}
$item = [
'field' => $field,
'title' => $def['label'],
'type' => $def['type'] ?? 'text',
'sortable' => true,
'search' => !empty($def['search']),
];
if (($def['type'] ?? '') === 'select') {
$item['options'] = $this->optionsOf($def);
}
$meta[] = $item;
}
return $meta;
}
protected function formatRow($row, $users = [])
{
$data = ['id' => (int)$row['id']];
foreach ($this->config['fields'] as $field => $def) {
if (!array_key_exists($field, $row)) {
continue;
}
$value = $row[$field];
$type = $def['type'] ?? 'text';
if ($type === 'user') {
$uid = (int)$value;
$data[$field] = $uid ? (isset($users[$uid]) ? $users[$uid] : '#' . $uid) : '-';
if (isset($data[$field . '_text']) === false) {
$data[$field . '_text'] = $data[$field];
}
} elseif ($type === 'select') {
$options = $this->optionsOf($def);
$data[$field] = isset($options[(string)$value]) ? $options[(string)$value] : $value;
} elseif ($type === 'bool') {
$data[$field] = $value ? '是' : '否';
} elseif ($type === 'datetime' || $type === 'date') {
$data[$field] = $this->timeText($field, $value);
} else {
$data[$field] = is_string($value) && mb_strlen($value) > 60 ? mb_substr($value, 0, 60) . '…' : $value;
}
if (isset($def['form']) && $def['form'] === false) {
unset($data[$field]);
}
}
if (isset($this->columns['deletetime'])) {
$data['is_deleted'] = !empty($row['deletetime']) ? 1 : 0;
}
return $data;
}
protected function formatRowForForm($row)
{
$data = $row;
foreach ($this->config['fields'] as $field => $def) {
if (!array_key_exists($field, $row)) {
continue;
}
$type = $def['type'] ?? 'text';
if ($type === 'datetime' || $type === 'date') {
$value = $row[$field];
if (is_numeric($value) && (int)$value > 1000000000) {
$data[$field] = $type === 'datetime' ? date('Y-m-d\TH:i', (int)$value) : date('Y-m-d', (int)$value);
} elseif (is_numeric($value) && (int)$value > 0 && (int)$value < 1000 && $type === 'datetime') {
// 秒数等小整数不转换
$data[$field] = $value;
}
}
if ($type === 'bool') {
$data[$field] = (int)$value;
}
}
return $data;
}
protected function collectPost()
{
$data = [];
foreach ($this->config['fields'] as $field => $def) {
if (isset($def['readonly']) && $def['readonly']) {
continue;
}
if (isset($def['form']) && $def['form'] === false) {
continue;
}
if (!isset($this->columns[$field])) {
continue;
}
$value = $this->request->post($field, null);
if ($value === null) {
continue;
}
$data[$field] = $this->castValue($field, $value);
}
return $data;
}
protected function castValue($field, $value)
{
$def = $this->config['fields'][$field] ?? [];
$type = $def['type'] ?? 'text';
if ($type === 'bool') {
return (int)!!$value;
}
if ($type === 'number') {
return (int)$value;
}
if ($type === 'decimal') {
return round((float)$value, 2);
}
if ($type === 'datetime') {
if ($value === '') {
return null;
}
$ts = strtotime(str_replace('T', ' ', (string)$value));
return $ts ?: null;
}
if ($type === 'date') {
return $value === '' ? null : date('Y-m-d', strtotime((string)$value));
}
if ($type === 'user') {
return (int)$value;
}
if ($type === 'textarea' || $type === 'text') {
return (string)$value;
}
return $value;
}
protected function timeText($field, $value)
{
if ($value === null || $value === '' || $value === '0') {
return '';
}
$columnType = $this->columns[$field] ?? 'varchar';
if (in_array($columnType, ['bigint', 'int', 'timestamp'], true)) {
$ts = (int)$value;
return $ts > 0 ? date('Y-m-d H:i', $ts) : '';
}
return (string)$value;
}
protected function optionsOf($def)
{
$options = $def['options'] ?? [];
if (is_string($options)) {
$options = ManageConfig::$enums[$options] ?? [];
}
return $options;
}
protected function describeTable($table)
{
$columns = [];
$rows = Db::query('SHOW COLUMNS FROM `' . str_replace('`', '', $table) . '`');
foreach ($rows as $row) {
$type = strtolower($row['Type']);
$columns[$row['Field']] = preg_match('/^(tiny|small|medium|big)?int/', $type) ? 'int'
: (preg_match('/^(decimal|float|double)/', $type) ? 'decimal'
: (preg_match('/^date$/', $type) ? 'date'
: (preg_match('/^(datetime|timestamp)/', $type) ? 'datetime' : 'varchar')));
}
return $columns;
}
protected function shortTable()
{
return str_replace('fa_', '', $this->table);
}
protected function parseIds($ids)
{
if (is_array($ids)) {
return array_map('intval', $ids);
}
return array_values(array_filter(array_map('intval', explode(',', (string)$ids))));
}
protected function userMap($rows)
{
$ids = [];
foreach ($rows as $row) {
if (isset($row['user_id']) && (int)$row['user_id'] > 0) {
$ids[] = (int)$row['user_id'];
}
if (isset($row['member_user_id']) && (int)$row['member_user_id'] > 0) {
$ids[] = (int)$row['member_user_id'];
}
}
if (!$ids) {
return [];
}
$users = Db::name('user')->whereIn('id', array_unique($ids))->column('nickname,username', 'id');
$map = [];
foreach ($users as $id => $item) {
$name = $item['nickname'] ?: $item['username'];
$map[$id] = $name . ' (#' . $id . ')';
}
return $map;
}
protected function userOptions()
{
$rows = Db::name('user')->order('id', 'desc')->limit(500)->field('id,username,nickname')->select();
$options = ['' => '未选择'];
foreach ($rows as $row) {
$options[$row['id']] = (($row['nickname'] ?: $row['username'])) . ' (#' . $row['id'] . ')';
}
return $options;
}
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 会员订单(会员中心)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_member_order
*/
class MemberOrder extends Manage
{
protected $fixedTable = 'fa_member_order';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 会员套餐(会员中心)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_member_plan
*/
class MemberPlan extends Manage
{
protected $fixedTable = 'fa_member_plan';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 纪念日(两性关系)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_memory
*/
class Memory extends Manage
{
protected $fixedTable = 'fa_memory';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 情绪日记(情绪疗愈)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_mood_log
*/
class MoodLog extends Manage
{
protected $fixedTable = 'fa_mood_log';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 爱心积分(家人共护)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_point_log
*/
class PointLog extends Manage
{
protected $fixedTable = 'fa_point_log';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 智能提醒(周期管理)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_reminder
*/
class Reminder extends Manage
{
protected $fixedTable = 'fa_reminder';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 症状标签(周期管理)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_symptom_tag
*/
class SymptomTag extends Manage
{
protected $fixedTable = 'fa_symptom_tag';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 任务管理(科学备孕)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_task
*/
class Task extends Manage
{
protected $fixedTable = 'fa_task';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 数据备份(会员中心)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_user_backup
*/
class UserBackup extends Manage
{
protected $fixedTable = 'fa_user_backup';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 隐私与提醒(会员中心)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_user_setting
*/
class UserSetting extends Manage
{
protected $fixedTable = 'fa_user_setting';
}

查看文件

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller;
/**
* 疫苗与儿保(亲子教育)
*
* 由通用 Manage 控制器提供列表/新增/编辑/删除能力,表:fa_vaccine
*/
class Vaccine extends Manage
{
protected $fixedTable = 'fa_vaccine';
}

查看文件

@@ -0,0 +1,104 @@
<?php
namespace app\admin\library;
/**
* 通用表单渲染器(运营后台)
*
* 根据 ManageConfig 中的字段定义渲染表单控件,保持所有业务模块表单风格一致。
*/
class FormBuilder
{
/**
* @param array $fields 字段定义(name => def)
* @param array $row 当前数据
* @param array $users 用户下拉数据
* @return string
*/
public static function render($fields, $row = [], $users = [])
{
$html = '';
foreach ($fields as $name => $def) {
if (isset($def['readonly']) && $def['readonly']) {
continue;
}
if (isset($def['form']) && $def['form'] === false) {
continue;
}
$html .= self::group($name, $def, $row, $users);
}
return $html;
}
protected static function group($name, $def, $row, $users)
{
$label = htmlspecialchars($def['label'] ?? $name);
$type = $def['type'] ?? 'text';
$value = array_key_exists($name, $row) ? $row[$name] : '';
$control = self::control($name, $type, $def, $value, $users);
$hint = isset($def['hint']) ? '<span class="help-block hq-hint">' . htmlspecialchars($def['hint']) . '</span>' : '';
return '<div class="form-group hq-form-group">'
. '<label class="control-label col-xs-12 col-sm-2">' . $label . '</label>'
. '<div class="col-xs-12 col-sm-8">' . $control . $hint . '</div>'
. '</div>';
}
protected static function control($name, $type, $def, $value, $users)
{
$n = htmlspecialchars($name);
$v = is_string($value) ? htmlspecialchars($value) : $value;
switch ($type) {
case 'textarea':
return '<textarea class="form-control hq-input" rows="4" name="' . $n . '" placeholder="' . htmlspecialchars($def['label'] ?? '') . '">'
. htmlspecialchars((string)$value) . '</textarea>';
case 'select':
$options = $def['options'] ?? [];
if (is_string($options)) {
$options = ManageConfig::$enums[$options] ?? [];
}
$html = '<select class="form-control hq-input" name="' . $n . '">';
if (!isset($def['required']) || !$def['required']) {
$html .= '<option value="">请选择</option>';
}
foreach ($options as $key => $text) {
$selected = ((string)$value === (string)$key) ? ' selected' : '';
$html .= '<option value="' . htmlspecialchars((string)$key) . '"' . $selected . '>' . htmlspecialchars($text) . '</option>';
}
return $html . '</select>';
case 'bool':
$checked = $value ? ' checked' : '';
return '<div class="hq-switch-wrap"><input type="hidden" name="' . $n . '" value="0"/>'
. '<label class="hq-switch"><input type="checkbox" name="' . $n . '" value="1"' . $checked . '/><span></span></label></div>';
case 'number':
return '<input type="number" step="1" class="form-control hq-input" name="' . $n . '" value="' . $v . '"/>';
case 'decimal':
return '<input type="number" step="0.01" class="form-control hq-input" name="' . $n . '" value="' . $v . '"/>';
case 'date':
return '<input type="date" class="form-control hq-input" name="' . $n . '" value="' . $v . '"/>';
case 'datetime':
return '<input type="datetime-local" class="form-control hq-input" name="' . $n . '" value="' . $v . '"/>';
case 'user':
$html = '<select class="form-control hq-input" name="' . $n . '">';
if (!$users) {
$html .= '<option value="">暂无用户</option>';
}
foreach ($users as $uid => $text) {
$selected = ((string)$value === (string)$uid) ? ' selected' : '';
$html .= '<option value="' . htmlspecialchars((string)$uid) . '"' . $selected . '>' . htmlspecialchars($text) . '</option>';
}
return $html . '</select>';
default:
return '<input type="text" class="form-control hq-input" name="' . $n . '" value="' . $v . '" placeholder="' . htmlspecialchars($def['label'] ?? '') . '"/>';
}
}
}

查看文件

@@ -0,0 +1,496 @@
<?php
namespace app\admin\library;
/**
* 运营后台业务模块注册表
*
* 与前端 M1–M9 功能模块一一对应,统一由 Manage 控制器提供列表/新增/编辑/删除能力。
* 字段定义支持:type(text/textarea/number/decimal/date/datetime/select/bool/json/user)、
* label、options、search、readonly、form(是否出现在表单)。
*/
class ManageConfig
{
/** 常用枚举 */
public static $enums = [
'status' => ['normal' => '正常', 'hidden' => '隐藏'],
'access' => ['free' => '免费', 'member' => '会员', 'pay' => '付费'],
'verify' => ['pending' => '待审核', 'passed' => '已通过', 'rejected' => '已驳回'],
'pay_status' => ['unpaid' => '待支付', 'paid' => '已支付', 'refunded' => '已退款'],
'scope' => ['prepare' => '备孕任务', 'family' => '家庭共育', 'relation' => '双人任务', 'parenting' => '育儿任务'],
'mood' => ['happy' => '开心', 'calm' => '平静', 'anxious' => '焦虑', 'low' => '低落', 'angry' => '烦躁'],
'mode' => ['cycle' => '经期模式', 'prepare' => '备孕模式', 'pregnant' => '孕期模式'],
'source' => ['user' => '用户记录', 'algorithm' => '算法生成'],
'result' => ['negative' => '阴性', 'weak' => '弱阳', 'positive' => '阳性', 'strong' => '强阳', 'unknown' => '未识别'],
'level' => ['basic' => '基础版', 'pro' => '专业版'],
'repeat_rule' => ['daily' => '每天', 'once' => '单次', 'weekly' => '每周'],
'app_lock' => ['none' => '关闭', 'password' => '数字密码', 'biometric' => '生物识别'],
'cert_verify' => ['pending' => '待审核', 'passed' => '已认证', 'expired' => '已过期'],
'consult_status' => ['pending' => '待开始', 'ongoing' => '进行中', 'finished' => '已完成', 'canceled' => '已取消'],
'onoff' => ['on' => '上架', 'off' => '下架'],
'relation' => ['partner' => '伴侣', 'parent' => '父母', 'friend' => '朋友'],
'test_type' => ['lh' => '排卵试纸', 'hcg' => '早孕试纸'],
'type_content' => [
'jijing' => '每日心语', 'mood' => '情绪科普', 'knowledge' => '知识库', 'advice' => '健康建议',
'meditation' => '正念冥想', 'care' => '暖心话术', 'card' => '沟通引导卡', 'conflict' => '冲突工具',
'firstaid' => '情绪急救包', 'guide' => '同房指导', 'course' => '系列课程', 'activity' => '亲子活动',
'emotion' => '育儿情绪',
],
'category_type' => [
'jijing' => '心语', 'mood' => '情绪科普', 'knowledge' => '知识库', 'advice' => '健康建议',
'meditation' => '冥想', 'care' => '话术', 'card' => '引导卡', 'conflict' => '冲突工具',
'firstaid' => '急救包', 'guide' => '同房指导', 'course' => '课程', 'activity' => '亲子活动', 'emotion' => '育儿情绪',
],
'action' => ['care_daily' => '每日关怀', 'prepare_task' => '备孕任务', 'exchange' => '积分兑换'],
'category' => ['check' => '检查', 'folic' => '叶酸', 'nutrition' => '营养', 'partner' => '伴侣', 'daily' => '日常', 'care' => '照护'],
'memory_type' => ['anniversary' => '纪念日', 'milestone' => '里程碑', 'daily' => '日常记录'],
'vaccine_type' => ['vaccine' => '疫苗', 'checkup' => '儿保体检'],
'gender' => ['0' => '未知', '1' => '男', '2' => '女'],
'advice_key' => [],
];
/**
* 所有受管业务表
*/
public static function all()
{
return [
/* ================= M1 周期管理 ================= */
'fa_cycle' => [
'title' => '周期档案',
'group' => '周期管理',
'sort' => 'start_date desc, id desc',
'fields' => [
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'start_date' => ['label' => '经期开始日', 'type' => 'date', 'search' => true],
'end_date' => ['label' => '经期结束日', 'type' => 'date'],
'period_length' => ['label' => '经期天数', 'type' => 'number'],
'cycle_length' => ['label' => '周期天数', 'type' => 'number'],
'ovulation_date' => ['label' => '排卵日', 'type' => 'date'],
'mode' => ['label' => '模式', 'type' => 'select', 'options' => 'mode'],
'source' => ['label' => '来源', 'type' => 'select', 'options' => 'source'],
],
],
'fa_cycle_log' => [
'title' => '每日症状记录',
'group' => '周期管理',
'sort' => 'log_date desc, id desc',
'fields' => [
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'log_date' => ['label' => '记录日期', 'type' => 'date', 'search' => true],
'flow' => ['label' => '经血量', 'type' => 'select', 'options' => ['0' => '无', '1' => '少', '2' => '中', '3' => '多']],
'pain_level' => ['label' => '痛经程度', 'type' => 'select', 'options' => ['0' => '无', '1' => '轻微', '2' => '可忍', '3' => '明显', '4' => '较重', '5' => '难忍']],
'symptom_ids' => ['label' => '症状标签ID', 'type' => 'text'],
'mood' => ['label' => '情绪分值', 'type' => 'number'],
'sleep_hours' => ['label' => '睡眠(小时)', 'type' => 'decimal'],
'sex' => ['label' => '性生活', 'type' => 'select', 'options' => ['0' => '无', '1' => '有保护', '2' => '无保护']],
'note' => ['label' => '备注', 'type' => 'textarea'],
],
],
'fa_symptom_tag' => [
'title' => '症状标签',
'group' => '周期管理',
'sort' => 'weigh desc, id asc',
'fields' => [
'name' => ['label' => '标签名', 'type' => 'text', 'search' => true],
'icon' => ['label' => '图标', 'type' => 'text'],
'category' => ['label' => '分类', 'type' => 'select', 'options' => ['body' => '身体', 'mood' => '情绪', 'flow' => '经血']],
'is_preset' => ['label' => '预设标签', 'type' => 'bool'],
'user_id' => ['label' => '归属用户', 'type' => 'user'],
'weigh' => ['label' => '权重', 'type' => 'number'],
'status' => ['label' => '状态', 'type' => 'select', 'options' => 'status'],
],
],
'fa_reminder' => [
'title' => '智能提醒',
'group' => '周期管理',
'sort' => 'id desc',
'fields' => [
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'title' => ['label' => '提醒内容', 'type' => 'text', 'search' => true],
'type' => ['label' => '类型', 'type' => 'select', 'options' => ['period' => '经期', 'ovulation' => '排卵', 'record' => '记录', 'medication' => '用药', 'custom' => '自定义']],
'remind_time' => ['label' => '提醒时间', 'type' => 'text'],
'repeat_rule' => ['label' => '重复', 'type' => 'select', 'options' => 'repeat_rule'],
'remind_date' => ['label' => '提醒日期', 'type' => 'date'],
'enabled' => ['label' => '启用', 'type' => 'bool'],
],
],
/* ================= M2 情绪疗愈 ================= */
'fa_mood_log' => [
'title' => '情绪日记',
'group' => '情绪疗愈',
'sort' => 'log_date desc, id desc',
'fields' => [
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'log_date' => ['label' => '记录日期', 'type' => 'date', 'search' => true],
'mood' => ['label' => '情绪', 'type' => 'select', 'options' => 'mood'],
'intensity' => ['label' => '强度', 'type' => 'number'],
'triggers' => ['label' => '触发事件', 'type' => 'text'],
'content' => ['label' => '内容', 'type' => 'textarea'],
],
],
/* ================= 内容中心 ================= */
'fa_content' => [
'title' => '内容库',
'group' => '内容中心',
'sort' => 'weigh desc, id desc',
'fields' => [
'type' => ['label' => '内容类型', 'type' => 'select', 'options' => 'type_content', 'search' => true],
'category_id' => ['label' => '分类ID', 'type' => 'number'],
'title' => ['label' => '标题', 'type' => 'text', 'search' => true],
'summary' => ['label' => '摘要', 'type' => 'text'],
'body' => ['label' => '正文', 'type' => 'textarea'],
'cover_emoji' => ['label' => '图标字符', 'type' => 'text'],
'media_type' => ['label' => '媒体类型', 'type' => 'select', 'options' => ['text' => '图文', 'audio' => '音频', 'video' => '视频']],
'duration' => ['label' => '时长(秒)', 'type' => 'number'],
'phase' => ['label' => '适用阶段', 'type' => 'text'],
'mood_tag' => ['label' => '适用情绪', 'type' => 'text'],
'extra' => ['label' => '扩展信息', 'type' => 'textarea'],
'access' => ['label' => '访问权限', 'type' => 'select', 'options' => 'access', 'search' => true],
'verify' => ['label' => '审核状态', 'type' => 'select', 'options' => 'verify', 'search' => true],
'weigh' => ['label' => '权重', 'type' => 'number'],
'status' => ['label' => '状态', 'type' => 'select', 'options' => 'status', 'search' => true],
],
],
'fa_content_category' => [
'title' => '内容分类',
'group' => '内容中心',
'sort' => 'weigh desc, id asc',
'fields' => [
'pid' => ['label' => '父级ID', 'type' => 'number'],
'name' => ['label' => '分类名', 'type' => 'text', 'search' => true],
'type' => ['label' => '类型', 'type' => 'select', 'options' => 'category_type', 'search' => true],
'weigh' => ['label' => '权重', 'type' => 'number'],
'status' => ['label' => '状态', 'type' => 'select', 'options' => 'status'],
],
],
/* ================= M3 科学备孕 ================= */
'fa_bbt_record' => [
'title' => '基础体温',
'group' => '科学备孕',
'sort' => 'log_date desc, id desc',
'fields' => [
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'log_date' => ['label' => '日期', 'type' => 'date', 'search' => true],
'temperature' => ['label' => '体温(℃)', 'type' => 'decimal'],
'measure_time' => ['label' => '测量时间', 'type' => 'text'],
'device' => ['label' => '设备来源', 'type' => 'select', 'options' => ['manual' => '手动', 'bluetooth' => '蓝牙']],
],
],
'fa_lh_test' => [
'title' => '试纸识别记录',
'group' => '科学备孕',
'sort' => 'log_date desc, id desc',
'fields' => [
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'test_type' => ['label' => '试纸类型', 'type' => 'select', 'options' => 'test_type'],
'log_date' => ['label' => '日期', 'type' => 'date', 'search' => true],
'result' => ['label' => '识别结果', 'type' => 'select', 'options' => 'result'],
'confidence' => ['label' => '置信度', 'type' => 'decimal'],
'algo' => ['label' => '算法', 'type' => 'select', 'options' => ['rule' => '规则引擎', 'cnn' => 'CNN 模型']],
'image' => ['label' => '试纸图片', 'type' => 'text'],
],
],
'fa_task' => [
'title' => '任务(备孕/双人/共育)',
'group' => '科学备孕',
'sort' => 'id desc',
'fields' => [
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'scope' => ['label' => '归属模块', 'type' => 'select', 'options' => 'scope', 'search' => true],
'title' => ['label' => '任务内容', 'type' => 'text', 'search' => true],
'category' => ['label' => '分类', 'type' => 'select', 'options' => 'category'],
'due_date' => ['label' => '截止日期', 'type' => 'date'],
'done' => ['label' => '已完成', 'type' => 'bool'],
'share_partner' => ['label' => '同步伴侣', 'type' => 'bool'],
],
],
/* ================= M4 家人共护 ================= */
'fa_family_bind' => [
'title' => '家人绑定',
'group' => '家人共护',
'sort' => 'id desc',
'fields' => [
'user_id' => ['label' => '主账号', 'type' => 'user', 'search' => true],
'member_user_id' => ['label' => '家人账号', 'type' => 'user'],
'member_name' => ['label' => '家人称呼', 'type' => 'text', 'search' => true],
'relation' => ['label' => '关系', 'type' => 'select', 'options' => 'relation'],
'scope' => ['label' => '可见范围', 'type' => 'text'],
'invite_code' => ['label' => '邀请码', 'type' => 'text', 'search' => true],
'status' => ['label' => '状态', 'type' => 'select', 'options' => ['pending' => '待接受', 'normal' => '已绑定', 'removed' => '已解绑'], 'search' => true],
],
],
'fa_care_message' => [
'title' => '沟通空间留言',
'group' => '家人共护',
'sort' => 'id desc',
'fields' => [
'bind_id' => ['label' => '绑定ID', 'type' => 'number', 'search' => true],
'user_id' => ['label' => '发送人', 'type' => 'user', 'search' => true],
'type' => ['label' => '类型', 'type' => 'select', 'options' => ['text' => '文字', 'voice' => '语音', 'gift' => '礼物']],
'content' => ['label' => '留言内容', 'type' => 'textarea'],
],
],
'fa_point_log' => [
'title' => '爱心积分流水',
'group' => '家人共护',
'sort' => 'id desc',
'fields' => [
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'action' => ['label' => '积分动作', 'type' => 'select', 'options' => 'action', 'search' => true],
'point' => ['label' => '积分', 'type' => 'number'],
'ref_id' => ['label' => '关联ID', 'type' => 'number'],
'remark' => ['label' => '备注', 'type' => 'text'],
],
],
/* ================= M7 两性关系 ================= */
'fa_memory' => [
'title' => '纪念日与回忆',
'group' => '两性关系',
'sort' => 'memory_date desc, id desc',
'fields' => [
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'title' => ['label' => '标题', 'type' => 'text', 'search' => true],
'memory_date' => ['label' => '日期', 'type' => 'date', 'search' => true],
'type' => ['label' => '类型', 'type' => 'select', 'options' => 'memory_type'],
'content' => ['label' => '内容', 'type' => 'textarea'],
'remind' => ['label' => '提醒', 'type' => 'bool'],
],
],
'fa_course_order' => [
'title' => '课程订单',
'group' => '两性关系',
'sort' => 'id desc',
'fields' => [
'order_no' => ['label' => '订单号', 'type' => 'text', 'search' => true],
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'content_id' => ['label' => '课程ID', 'type' => 'number'],
'amount' => ['label' => '金额', 'type' => 'decimal'],
'pay_status' => ['label' => '支付状态', 'type' => 'select', 'options' => 'pay_status', 'search' => true],
],
],
/* ================= M8 亲子教育 ================= */
'fa_baby' => [
'title' => '宝宝档案',
'group' => '亲子教育',
'sort' => 'id desc',
'fields' => [
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'nickname' => ['label' => '小名', 'type' => 'text', 'search' => true],
'gender' => ['label' => '性别', 'type' => 'select', 'options' => ['0' => '未知', '1' => '男', '2' => '女']],
'birthday' => ['label' => '生日', 'type' => 'date'],
'due_date' => ['label' => '预产期', 'type' => 'date'],
'status' => ['label' => '状态', 'type' => 'select', 'options' => 'status'],
],
],
'fa_growth_record' => [
'title' => '成长记录',
'group' => '亲子教育',
'sort' => 'record_date desc, id desc',
'fields' => [
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'baby_id' => ['label' => '宝宝ID', 'type' => 'number'],
'record_date' => ['label' => '记录日期', 'type' => 'date', 'search' => true],
'height' => ['label' => '身高(cm)', 'type' => 'decimal'],
'weight' => ['label' => '体重(kg)', 'type' => 'decimal'],
'milestone' => ['label' => '里程碑', 'type' => 'text'],
'note' => ['label' => '备注', 'type' => 'textarea'],
],
],
'fa_vaccine' => [
'title' => '疫苗与儿保',
'group' => '亲子教育',
'sort' => 'weigh desc, id asc',
'fields' => [
'name' => ['label' => '项目名称', 'type' => 'text', 'search' => true],
'type' => ['label' => '类型', 'type' => 'select', 'options' => 'vaccine_type', 'search' => true],
'month_from' => ['label' => '起始月龄', 'type' => 'number'],
'month_to' => ['label' => '结束月龄', 'type' => 'number'],
'note' => ['label' => '说明', 'type' => 'text'],
'weigh' => ['label' => '权重', 'type' => 'number'],
],
],
/* ================= M9 心理服务 ================= */
'fa_counselor' => [
'title' => '咨询师',
'group' => '心理服务',
'sort' => 'weigh desc, id asc',
'fields' => [
'name' => ['label' => '姓名', 'type' => 'text', 'search' => true],
'avatar_emoji' => ['label' => '头像图标', 'type' => 'text'],
'title' => ['label' => '职称', 'type' => 'text'],
'cert_no' => ['label' => '资质编号', 'type' => 'text', 'search' => true],
'cert_verify' => ['label' => '资质审核', 'type' => 'select', 'options' => 'cert_verify', 'search' => true],
'price_text' => ['label' => '文字咨询价', 'type' => 'decimal'],
'price_voice' => ['label' => '语音咨询价', 'type' => 'decimal'],
'price_video' => ['label' => '视频咨询价', 'type' => 'decimal'],
'rate' => ['label' => '平台分成', 'type' => 'decimal'],
'intro' => ['label' => '简介', 'type' => 'textarea'],
'tags' => ['label' => '擅长标签', 'type' => 'text'],
'status' => ['label' => '状态', 'type' => 'select', 'options' => 'onoff', 'search' => true],
'weigh' => ['label' => '权重', 'type' => 'number'],
],
],
'fa_consult_order' => [
'title' => '咨询订单',
'group' => '心理服务',
'sort' => 'id desc',
'fields' => [
'order_no' => ['label' => '订单号', 'type' => 'text', 'search' => true],
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'counselor_id' => ['label' => '咨询师ID', 'type' => 'number'],
'mode' => ['label' => '方式', 'type' => 'select', 'options' => ['text' => '文字', 'voice' => '语音', 'video' => '视频']],
'start_time' => ['label' => '预约时间', 'type' => 'datetime'],
'duration' => ['label' => '时长(分钟)', 'type' => 'number'],
'amount' => ['label' => '金额', 'type' => 'decimal'],
'pay_status' => ['label' => '支付状态', 'type' => 'select', 'options' => 'pay_status', 'search' => true],
'consult_status' => ['label' => '咨询状态', 'type' => 'select', 'options' => 'consult_status', 'search' => true],
'rate_amount' => ['label' => '分成金额', 'type' => 'decimal'],
],
],
'fa_assessment' => [
'title' => '量表管理',
'group' => '心理服务',
'sort' => 'weigh desc, id asc',
'fields' => [
'code' => ['label' => '量表编码', 'type' => 'text', 'search' => true],
'title' => ['label' => '量表名称', 'type' => 'text', 'search' => true],
'intro' => ['label' => '引导语', 'type' => 'textarea'],
'questions' => ['label' => '题目(JSON数组)', 'type' => 'textarea'],
'scoring' => ['label' => '分级规则(JSON)', 'type' => 'textarea'],
'level' => ['label' => '版本', 'type' => 'select', 'options' => 'level'],
'scope' => ['label' => '归属', 'type' => 'select', 'options' => ['mood' => '情绪', 'relation' => '关系', 'parenting' => '育儿']],
'access' => ['label' => '访问权限', 'type' => 'select', 'options' => 'access'],
'status' => ['label' => '状态', 'type' => 'select', 'options' => 'status'],
'weigh' => ['label' => '权重', 'type' => 'number'],
],
],
'fa_assessment_record' => [
'title' => '测评记录',
'group' => '心理服务',
'sort' => 'id desc',
'fields' => [
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'assessment_id' => ['label' => '量表ID', 'type' => 'number', 'search' => true],
'score' => ['label' => '得分', 'type' => 'decimal'],
'result' => ['label' => '结果分级', 'type' => 'text'],
'risk_flag' => ['label' => '风险标记', 'type' => 'bool'],
'answers' => ['label' => '作答(JSON)', 'type' => 'textarea', 'readonly' => true],
'report' => ['label' => '解读', 'type' => 'textarea', 'readonly' => true],
],
],
/* ================= M6 会员与用户 ================= */
'fa_member_plan' => [
'title' => '会员套餐',
'group' => '会员中心',
'sort' => 'weigh desc, id asc',
'fields' => [
'name' => ['label' => '套餐名', 'type' => 'text', 'search' => true],
'code' => ['label' => '编码', 'type' => 'text', 'search' => true],
'subtitle' => ['label' => '副标题', 'type' => 'text'],
'price' => ['label' => '售价', 'type' => 'decimal'],
'origin_price' => ['label' => '原价', 'type' => 'decimal'],
'duration_days' => ['label' => '有效天数', 'type' => 'number'],
'benefits' => ['label' => '权益(JSON)', 'type' => 'textarea'],
'badge' => ['label' => '角标', 'type' => 'text'],
'weigh' => ['label' => '权重', 'type' => 'number'],
'status' => ['label' => '状态', 'type' => 'select', 'options' => 'status'],
],
],
'fa_member_order' => [
'title' => '会员订单',
'group' => '会员中心',
'sort' => 'id desc',
'fields' => [
'order_no' => ['label' => '订单号', 'type' => 'text', 'search' => true],
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'plan_code' => ['label' => '套餐编码', 'type' => 'text'],
'amount' => ['label' => '金额', 'type' => 'decimal'],
'pay_type' => ['label' => '支付方式', 'type' => 'select', 'options' => ['wechat' => '微信', 'alipay' => '支付宝']],
'pay_status' => ['label' => '支付状态', 'type' => 'select', 'options' => 'pay_status', 'search' => true],
'trade_no' => ['label' => '第三方流水', 'type' => 'text'],
'start_time' => ['label' => '生效时间', 'type' => 'datetime'],
'expire_time' => ['label' => '到期时间', 'type' => 'datetime'],
],
],
'fa_user_setting' => [
'title' => '隐私与提醒设置',
'group' => '会员中心',
'sort' => 'id desc',
'fields' => [
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'app_lock' => ['label' => '应用锁', 'type' => 'select', 'options' => 'app_lock'],
'local_only' => ['label' => '仅本地存储', 'type' => 'bool'],
'remind_json' => ['label' => '提醒配置(JSON)', 'type' => 'textarea'],
],
],
'fa_user_backup' => [
'title' => '数据备份',
'group' => '会员中心',
'sort' => 'id desc',
'fields' => [
'user_id' => ['label' => '用户', 'type' => 'user', 'search' => true],
'name' => ['label' => '备份名称', 'type' => 'text'],
'size' => ['label' => '大小(字节)', 'type' => 'number', 'readonly' => true],
'payload' => ['label' => '备份内容', 'type' => 'textarea', 'readonly' => true],
],
],
'fa_user' => [
'title' => '会员用户',
'group' => '会员中心',
'sort' => 'id desc',
'readonly_table' => true,
'fields' => [
'username' => ['label' => '账号', 'type' => 'text', 'search' => true],
'nickname' => ['label' => '昵称', 'type' => 'text', 'search' => true],
'mobile' => ['label' => '手机号', 'type' => 'text', 'search' => true],
'gender' => ['label' => '性别', 'type' => 'select', 'options' => ['0' => '未知', '1' => '男', '2' => '女']],
'birthday' => ['label' => '生日', 'type' => 'date'],
'bio' => ['label' => '签名', 'type' => 'text'],
'score' => ['label' => '积分', 'type' => 'number'],
'status' => ['label' => '状态', 'type' => 'select', 'options' => ['normal' => '正常', 'hidden' => '禁用']],
],
],
];
}
/**
* 取单个表的配置,并补齐通用字段
*/
public static function get($table)
{
$all = self::all();
if (!isset($all[$table])) {
return null;
}
$config = $all[$table];
if (!isset($config['fields']['createtime'])) {
$config['fields']['createtime'] = ['label' => '创建时间', 'type' => 'datetime', 'readonly' => true];
}
if (!isset($config['fields']['updatetime'])) {
$config['fields']['updatetime'] = ['label' => '更新时间', 'type' => 'datetime', 'readonly' => true];
}
if (!isset($config['fields']['deletetime'])) {
$config['fields']['deletetime'] = ['label' => '删除时间', 'type' => 'datetime', 'readonly' => true, 'form' => false];
}
return $config;
}
/** 分组(用于菜单生成与文档) */
public static function groups()
{
$groups = [];
foreach (self::all() as $table => $config) {
$groups[$config['group']][] = ['table' => $table, 'title' => $config['title']];
}
return $groups;
}
}

查看文件

@@ -1,9 +1,24 @@
<!-- Logo -->
<!-- Logo(花期品牌) -->
<style>
.hq-logo-lg { display: inline-flex; align-items: center; gap: 8px; }
.hq-logo-lg svg { flex-shrink: 0; }
.hq-logo-mini { letter-spacing: 1px; }
</style>
<a href="javascript:;" class="logo">
<!-- 迷你模式下Logo的大小为50X50 -->
<span class="logo-mini">{$site.name|mb_substr=0,4,'utf-8'|mb_strtoupper='utf-8'|htmlentities}</span>
<span class="logo-mini hq-logo-mini">花期</span>
<!-- 普通模式下Logo -->
<span class="logo-lg">{$site.name|htmlentities}</span>
<span class="logo-lg hq-logo-lg">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="#FFD8C9" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="2.6"/>
<circle cx="12" cy="5.4" r="2.9"/>
<circle cx="18.6" cy="12" r="2.9"/>
<circle cx="12" cy="18.6" r="2.9"/>
<circle cx="5.4" cy="12" r="2.9"/>
</svg>
<span>{$site.name|htmlentities}</span>
</span>
</a>
<!-- 顶部通栏样式 -->

查看文件

@@ -1,142 +1,376 @@
<!DOCTYPE html>
<html>
<html lang="zh-cn">
<head>
{include file="common/meta" /}
<style type="text/css">
:root {
--brand: #E8604C;
--brand-light: #F58C7C;
--brand-peach: #F5A65B;
--brand-brown: #C79A7B;
--text: #3E2C27;
--text-2: #8C7367;
--text-3: #B9A79E;
--line: #F5E7DF;
--cream: #FFF9F5;
}
* { box-sizing: border-box; }
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body {
color: #999;
background-color: #f1f4fd;
background-size: cover;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", "Microsoft YaHei", Arial, sans-serif;
color: var(--text);
background:
radial-gradient(circle at 12% 18%, rgba(245, 166, 91, 0.28), transparent 42%),
radial-gradient(circle at 88% 8%, rgba(232, 96, 76, 0.20), transparent 40%),
radial-gradient(circle at 78% 92%, rgba(199, 154, 123, 0.22), transparent 46%),
linear-gradient(160deg, #FFF6EF 0%, #FFFBF7 45%, #FFF3EC 100%);
background-attachment: fixed;
}
a {
color: #444;
a { color: var(--brand); }
a:hover { color: #C9482F; }
.hq-login {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
}
.login-screen {
max-width: 430px;
padding: 0;
margin: 100px auto 0 auto;
.hq-login-wrap {
width: 100%;
max-width: 940px;
display: flex;
align-items: stretch;
background: #FFFFFF;
border-radius: 28px;
overflow: hidden;
box-shadow: 0 30px 80px rgba(232, 96, 76, 0.16), 0 2px 6px rgba(62, 44, 39, 0.04);
}
.login-screen .well {
border-radius: 3px;
-webkit-box-shadow: 0 0 30px rgba(0, 0, 0, 0.1);
box-shadow: 0 0 30px rgba(0, 0, 0, 0.1);
background: rgba(255, 255, 255, 1);
border: none;
/*overflow: hidden;*/
padding: 0;
}
@media (max-width: 767px) {
.login-screen {
padding: 0 20px;
}
}
.profile-img-card {
width: 100px;
height: 100px;
display: block;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
border-radius: 50%;
margin: -93px auto 30px;
border: 5px solid #fff;
}
.profile-name-card {
text-align: center;
}
.login-head {
background: #899fe1;
border-radius: 3px 3px 0 0;
}
.login-form {
padding: 40px 30px;
/* ---------- 左侧品牌区 ---------- */
.hq-brand-panel {
width: 44%;
padding: 56px 44px;
color: #FFFFFF;
background: linear-gradient(150deg, #F58C7C 0%, #E8604C 58%, #D9503C 100%);
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
z-index: 99;
overflow: hidden;
}
#login-form {
margin-top: 20px;
.hq-brand-panel::after {
content: "";
position: absolute;
right: -80px;
bottom: -90px;
width: 260px;
height: 260px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.10);
}
#login-form .input-group {
margin-bottom: 15px;
.hq-brand-panel::before {
content: "";
position: absolute;
right: 42px;
top: 38px;
width: 120px;
height: 120px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
}
#login-form .form-control {
.hq-logo {
width: 64px;
height: 64px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.18);
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(2px);
}
.hq-brand-name {
margin: 26px 0 6px;
font-size: 34px;
font-weight: 600;
letter-spacing: 8px;
}
.hq-brand-slogan {
font-size: 13px;
letter-spacing: 3px;
opacity: 0.86;
}
</style>
<!--@formatter:off-->
{if $background}
<style type="text/css">
body{
background-image: url('{$background|htmlentities}');
.hq-brand-desc {
position: relative;
font-size: 13px;
line-height: 2;
opacity: 0.9;
margin: 0;
padding-left: 14px;
}
.hq-brand-desc::before {
content: "";
position: absolute;
left: 0;
top: 8px;
width: 3px;
height: calc(100% - 16px);
border-radius: 3px;
background: rgba(255, 255, 255, 0.5);
}
/* ---------- 右侧表单区 ---------- */
.hq-form-panel {
width: 56%;
padding: 56px 56px 44px;
display: flex;
flex-direction: column;
justify-content: center;
}
.hq-form-title {
margin: 0 0 6px;
font-size: 24px;
font-weight: 600;
color: var(--text);
letter-spacing: 1px;
}
.hq-form-sub {
margin: 0 0 30px;
font-size: 13px;
color: var(--text-3);
}
.hq-field {
position: relative;
margin-bottom: 16px;
}
.hq-field-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
opacity: 0.55;
pointer-events: none;
}
.hq-field .form-control {
height: 52px;
padding: 6px 16px 6px 48px;
font-size: 14px;
color: var(--text);
background-color: var(--cream);
border: 1px solid var(--line);
border-radius: 14px;
box-shadow: none;
transition: border-color .18s ease, box-shadow .18s ease, background-color .18s ease;
}
.hq-field .form-control::placeholder { color: var(--text-3); }
.hq-field .form-control:focus {
background-color: #FFFFFF;
border-color: var(--brand-light);
box-shadow: 0 0 0 4px rgba(232, 96, 76, 0.10);
}
.hq-field-captcha { display: flex; align-items: center; }
.hq-field-captcha .form-control { padding-right: 130px; }
.hq-captcha {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
height: 38px;
border-radius: 10px;
overflow: hidden;
cursor: pointer;
border: 1px solid var(--line);
background: #FFFFFF;
}
.hq-captcha img { display: block; height: 38px; width: 110px; }
.hq-keep {
display: flex;
align-items: center;
margin: 4px 0 22px;
font-size: 13px;
color: var(--text-2);
cursor: pointer;
user-select: none;
}
.hq-keep input { margin: 0 8px 0 0; accent-color: var(--brand); }
.hq-submit {
width: 100%;
height: 52px;
border: none;
border-radius: 14px;
font-size: 16px;
font-weight: 500;
letter-spacing: 4px;
color: #FFFFFF;
background: linear-gradient(135deg, #F58C7C 0%, #E8604C 100%);
box-shadow: 0 14px 28px rgba(232, 96, 76, 0.26);
transition: transform .15s ease, box-shadow .15s ease, filter .15s ease;
}
.hq-submit:hover, .hq-submit:focus {
filter: brightness(1.03);
box-shadow: 0 18px 32px rgba(232, 96, 76, 0.30);
outline: none;
}
.hq-submit:active { transform: translateY(1px) scale(0.998); }
.hq-foot {
margin-top: 26px;
font-size: 12px;
color: var(--text-3);
text-align: center;
line-height: 1.9;
}
.hq-foot a { color: var(--text-2); }
#errtips {
margin-bottom: 14px;
padding: 10px 14px;
font-size: 13px;
color: #C9482F;
background: #FFF2EE;
border: 1px solid #F8D7CF;
border-radius: 10px;
}
/* 头像/名称占位(兼容后台登录脚本读取) */
.hq-hidden { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
/* ---------- 响应式 ---------- */
@media (max-width: 860px) {
.hq-login-wrap { max-width: 460px; }
.hq-brand-panel { display: none; }
.hq-form-panel { width: 100%; padding: 44px 32px 32px; }
}
@media (max-width: 480px) {
.hq-login { padding: 0; }
.hq-login-wrap { border-radius: 0; min-height: 100vh; }
.hq-form-panel { padding: 56px 26px 32px; }
}
</style>
{/if}
<!--@formatter:on-->
</head>
<body>
<div class="container">
<div class="login-wrapper">
<div class="login-screen">
<div class="well">
<div class="login-head">
<img src="__CDN__/assets/img/login-head.png" style="width:100%;"/>
<div class="hq-login">
<div class="hq-login-wrap">
<!-- 品牌区 -->
<div class="hq-brand-panel">
<div>
<div class="hq-logo">
<svg xmlns="http://www.w3.org/2000/svg" width="34" height="34" viewBox="0 0 24 24"
fill="none" stroke="#FFFFFF" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="2.6"/>
<circle cx="12" cy="5.4" r="2.9"/>
<circle cx="18.6" cy="12" r="2.9"/>
<circle cx="12" cy="18.6" r="2.9"/>
<circle cx="5.4" cy="12" r="2.9"/>
</svg>
</div>
<div class="hq-brand-name">花期</div>
<div class="hq-brand-slogan">温柔记录 · 静待花开</div>
</div>
<p class="hq-brand-desc">
运营管理后台<br>
内容审核 · 用户运营 · 数据看板
</p>
</div>
<div class="login-form">
<img id="profile-img" class="profile-img-card" src="__CDN__/assets/img/avatar.png"/>
<p id="profile-name" class="profile-name-card"></p>
<form action="" method="post" id="login-form">
<!--@AdminLoginFormBegin-->
<!-- 表单区 -->
<div class="hq-form-panel">
<h1 class="hq-form-title">登录运营后台</h1>
<p class="hq-form-sub">请使用管理员账号登录,所有操作将被记录</p>
<img id="profile-img" src="__CDN__/assets/img/avatar.png" class="hq-hidden" alt=""/>
<input type="text" id="profile-name" class="hq-hidden" value=""/>
<form action="" method="post" id="login-form" autocomplete="off">
<div id="errtips" class="hide"></div>
{:token()}
<div class="input-group">
<div class="input-group-addon"><span class="glyphicon glyphicon-user" aria-hidden="true"></span></div>
<input type="text" class="form-control" id="pd-form-username" placeholder="{:__('Username')}" name="username" autocomplete="off" value="" data-rule="{:__('Username')}:required;username"/>
<div class="hq-field">
<svg class="hq-field-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="#8C7367" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="8" r="3.6"/>
<path d="M4.8 20.4a7.4 7.4 0 0 1 14.4 0"/>
</svg>
<input type="text" class="form-control" id="pd-form-username" placeholder="{:__('Username')}"
name="username" value="" data-rule="{:__('Username')}:required;username"/>
</div>
<div class="input-group">
<div class="input-group-addon"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span></div>
<input type="password" class="form-control" id="pd-form-password" placeholder="{:__('Password')}" name="password" autocomplete="off" value="" data-rule="{:__('Password')}:required;password"/>
<div class="hq-field">
<svg class="hq-field-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="#8C7367" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="4.6" y="10.4" width="14.8" height="9.6" rx="2.4"/>
<path d="M8.2 10.4V8a3.8 3.8 0 0 1 7.6 0v2.4M12 14.4v2"/>
</svg>
<input type="password" class="form-control" id="pd-form-password" placeholder="{:__('Password')}"
name="password" value="" data-rule="{:__('Password')}:required;password"/>
</div>
<!--@CaptchaBegin-->
{if $Think.config.fastadmin.login_captcha}
<div class="input-group">
<div class="input-group-addon"><span class="glyphicon glyphicon-option-horizontal" aria-hidden="true"></span></div>
<input type="text" name="captcha" class="form-control" placeholder="{:__('Captcha')}" data-rule="{:__('Captcha')}:required;length({$Think.config.captcha.length|htmlentities})" autocomplete="off"/>
<span class="input-group-addon" style="padding:0;border:none;cursor:pointer;">
<img src="{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha" width="100" height="30" onclick="this.src = '{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha&r=' + Math.random();"/>
<div class="hq-field hq-field-captcha">
<svg class="hq-field-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="#8C7367" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="3.4" y="5" width="17.2" height="15.6" rx="2.6"/>
<path d="M7.6 11h8.8M7.6 15h5.4"/>
</svg>
<input type="text" name="captcha" class="form-control" placeholder="{:__('Captcha')}"
data-rule="{:__('Captcha')}:required;length({$Think.config.captcha.length|htmlentities})"
autocomplete="off"/>
<span class="hq-captcha">
<img src="{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha" width="110" height="38"
onclick="this.src = '{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha&r=' + Math.random();"/>
</span>
</div>
{/if}
<!--@CaptchaEnd-->
{if $keeyloginhours>0}
<div class="form-group checkbox">
<label class="inline" for="keeplogin" data-toggle="tooltip" title="{:__('The duration of the session is %s hours', $keeyloginhours)}">
<label class="hq-keep" for="keeplogin"
data-toggle="tooltip" title="{:__('The duration of the session is %s hours', $keeyloginhours)}">
<input type="checkbox" name="keeplogin" id="keeplogin" value="1"/>
{:__('Keep login')}
</label>
</div>
{/if}
<div class="form-group">
<button type="submit" class="btn btn-success btn-lg btn-block" style="background:#708eea;">{:__('Sign in')}</button>
</div>
<!--@AdminLoginFormEnd-->
<button type="submit" class="hq-submit">{:__('Sign in')}</button>
</form>
</div>
<div class="hq-foot">
花期运营中心 · 内容仅供参考,不能替代医疗诊断<br>
<a href="__PUBLIC__" target="_blank">查看前台站点</a>
</div>
</div>
</div>

查看文件

@@ -0,0 +1,21 @@
<div class="panel panel-default panel-intro hq-admin">
{include file="manage/style" /}
<div class="panel-heading">
<div class="panel-lead">
<em>{$tableGroup}</em>
<span style="margin-left:6px;">新增{$tableTitle}</span>
</div>
</div>
<div class="panel-body">
<form id="add-form" class="form-horizontal hq-admin-form" role="form" data-toggle="validator" method="post" action="">
{$formHtml}
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2"></label>
<div class="col-xs-12 col-sm-8">
<button type="submit" class="btn hq-btn-primary">{:__('Submit')}</button>
<button type="button" class="btn btn-default btn-return">{:__('Back')}</button>
</div>
</div>
</form>
</div>
</div>

查看文件

@@ -0,0 +1,21 @@
<div class="panel panel-default panel-intro hq-admin">
{include file="manage/style" /}
<div class="panel-heading">
<div class="panel-lead">
<em>{$tableGroup}</em>
<span style="margin-left:6px;">编辑{$tableTitle}</span>
</div>
</div>
<div class="panel-body">
<form id="edit-form" class="form-horizontal hq-admin-form" role="form" data-toggle="validator" method="post" action="">
{$formHtml}
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2"></label>
<div class="col-xs-12 col-sm-8">
<button type="submit" class="btn hq-btn-primary">{:__('Submit')}</button>
<button type="button" class="btn btn-default btn-return">{:__('Back')}</button>
</div>
</div>
</form>
</div>
</div>

查看文件

@@ -0,0 +1,55 @@
<div class="panel panel-default panel-intro hq-admin">
<style>
.hq-admin .panel-heading { background: #fff; border-bottom: 1px solid #F5E7DF; }
.hq-admin .panel-lead em { color: #E8604C; font-style: normal; font-weight: 600; }
.hq-admin .toolbar { padding: 12px 0; }
.hq-admin .toolbar .btn-primary { background: linear-gradient(135deg, #F58C7C 0%, #E8604C 100%); border-color: #E8604C; }
.hq-admin .toolbar .btn-primary:hover, .hq-admin .toolbar .btn-primary:focus { filter: brightness(1.04); border-color: #E8604C; }
.hq-admin .hq-tip { line-height: 34px; }
.hq-admin .hq-tip b { color: #E8604C; }
.hq-admin .hq-table thead th { background: #FFF9F5; color: #8C7367; font-weight: 600; border-bottom: 1px solid #F5E7DF; }
.hq-admin .hq-table > tbody > tr > td { vertical-align: middle; color: #3E2C27; }
.hq-admin .hq-table > tbody > tr:hover > td { background: #FFFBF8; }
.hq-admin .fixed-table-container { border: 1px solid #F5E7DF; border-radius: 10px; }
.hq-admin .btn-success { background: #7FB77E; border-color: #7FB77E; }
.hq-admin .pagination > .active > a, .hq-admin .pagination > .active > span { background: #E8604C; border-color: #E8604C; }
.hq-admin .search-input { border-radius: 8px; }
</style>
<div class="panel-heading">
<div class="panel-lead">
<em>{$tableGroup}</em>
<span style="margin-left:6px;">{$tableTitle}</span>
</div>
</div>
<div class="panel-body">
<div id="toolbar" class="toolbar">
{if $readonlyTable}
{:build_toolbar('refresh')}
{else /}
{:build_toolbar('refresh,add,edit,del')}
<div class="dropdown btn-group">
<a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown">
<i class="fa fa-cog"></i> 批量
</a>
<ul class="dropdown-menu text-left" role="menu">
<li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=normal"><i class="fa fa-check"></i> 设为正常</a></li>
<li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=hidden"><i class="fa fa-eye-slash"></i> 设为隐藏</a></li>
<li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="verify=passed"><i class="fa fa-check-circle"></i> 审核通过</a></li>
<li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="verify=rejected"><i class="fa fa-times-circle"></i> 驳回</a></li>
</ul>
</div>
{/if}
<div class="pull-right">
<span class="text-muted hq-tip">{$tableGroup} / {$tableTitle} · 共 <b id="hq-total">0</b> 条</span>
</div>
</div>
<table id="table" class="table table-hover hq-table" data-table="{$table}"
data-operate-edit="{:$auth->check('manage/edit')}"
data-operate-del="{:$auth->check('manage/del')}"
width="100%">
</table>
</div>
</div>

查看文件

@@ -0,0 +1,49 @@
<style>
.hq-admin { --brand: #E8604C; }
.hq-admin .panel-heading { background: #fff; border-bottom: 1px solid #F5E7DF; }
.hq-admin .panel-lead em { color: #E8604C; font-style: normal; font-weight: 600; }
.hq-admin-form { padding: 10px 0 0; }
.hq-admin-form .hq-form-group { margin-bottom: 18px; }
.hq-admin-form .control-label { color: #8C7367; font-weight: 500; padding-top: 9px; }
.hq-admin-form .hq-input {
height: 40px;
border-radius: 10px;
border: 1px solid #F5E7DF;
background: #FFF9F5;
box-shadow: none;
color: #3E2C27;
font-size: 13px;
transition: border-color .16s ease, box-shadow .16s ease, background .16s ease;
}
.hq-admin-form textarea.hq-input { height: auto; min-height: 96px; line-height: 1.7; }
.hq-admin-form .hq-input:focus {
border-color: #F58C7C;
background: #fff;
box-shadow: 0 0 0 4px rgba(232, 96, 76, .10);
}
.hq-admin-form .hq-btn-primary {
height: 40px;
padding: 0 26px;
border: none;
border-radius: 10px;
color: #fff;
letter-spacing: 2px;
background: linear-gradient(135deg, #F58C7C 0%, #E8604C 100%);
box-shadow: 0 10px 20px rgba(232, 96, 76, .20);
}
.hq-admin-form .hq-btn-primary:hover, .hq-admin-form .hq-btn-primary:focus { color: #fff; filter: brightness(1.04); }
.hq-admin-form .hq-hint { color: #B9A79E; font-size: 12px; }
.hq-switch { position: relative; display: inline-block; width: 46px; height: 26px; margin: 7px 0 0; }
.hq-switch input { opacity: 0; width: 0; height: 0; }
.hq-switch span {
position: absolute; cursor: pointer; inset: 0;
background-color: #EFE2DA; border-radius: 26px; transition: background .18s ease;
}
.hq-switch span::before {
content: ""; position: absolute; height: 20px; width: 20px; left: 3px; top: 3px;
background: #fff; border-radius: 50%; transition: transform .18s ease;
box-shadow: 0 2px 6px rgba(62, 44, 39, .18);
}
.hq-switch input:checked + span { background: linear-gradient(135deg, #F58C7C 0%, #E8604C 100%); }
.hq-switch input:checked + span::before { transform: translateX(20px); }
</style>

查看文件

@@ -1,7 +1,7 @@
<?php
return [
'name' => '我的网站',
'name' => '花期运营中心',
'beian' => '',
'cdnurl' => '',
'version' => '1.0.1',

55
server/db/05_admin_menu.sql 普通文件
查看文件

@@ -0,0 +1,55 @@
-- ============================================================
-- 花期 APP · 运营后台业务模块菜单(与前端 M1–M9 对齐)
-- 可重复执行:清理旧的 manage 菜单后重建
-- 说明:rule.name 需与「控制器/方法」小写形式一致(auth 校验用),
-- url 使用下划线形式(ThinkPHP 会转换为驼峰类名)
-- ============================================================
SET NAMES utf8mb4;
SET @pid := (SELECT id FROM `fa_auth_rule` WHERE `name` = 'manage' AND `pid` = 0 LIMIT 1);
DELETE FROM `fa_auth_rule` WHERE `pid` = @pid;
DELETE FROM `fa_auth_rule` WHERE `name` = 'manage' AND `pid` = 0;
DELETE FROM `fa_auth_rule` WHERE `name` LIKE 'manage/%';
INSERT INTO `fa_auth_rule` (`type`,`pid`,`name`,`title`,`icon`,`ismenu`,`weigh`,`status`,`createtime`,`updatetime`)
VALUES ('file',0,'manage','花期运营','fa fa-heart',1,120,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP());
SET @parent := LAST_INSERT_ID();
INSERT INTO `fa_auth_rule` (`type`,`pid`,`name`,`title`,`icon`,`url`,`ismenu`,`weigh`,`status`,`createtime`,`updatetime`) VALUES
('file',@parent,'cycle/index','周期档案','fa fa-calendar','cycle/index',1,200,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'cyclelog/index','每日症状记录','fa fa-pencil-square-o','cycle_log/index',1,199,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'symptomtag/index','症状标签','fa fa-tags','symptom_tag/index',1,198,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'reminder/index','智能提醒','fa fa-bell-o','reminder/index',1,197,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'moodlog/index','情绪日记','fa fa-heartbeat','mood_log/index',1,196,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'content/index','内容库','fa fa-file-text-o','content/index',1,195,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'contentcategory/index','内容分类','fa fa-list-alt','content_category/index',1,194,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'bbtrecord/index','基础体温','fa fa-thermometer-half','bbt_record/index',1,193,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'lhtest/index','试纸识别','fa fa-flask','lh_test/index',1,192,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'task/index','任务管理','fa fa-tasks','task/index',1,191,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'familybind/index','家人绑定','fa fa-users','family_bind/index',1,190,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'caremessage/index','沟通留言','fa fa-comments-o','care_message/index',1,189,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'pointlog/index','爱心积分','fa fa-diamond','point_log/index',1,188,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'memory/index','纪念日','fa fa-heart','memory/index',1,187,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'courseorder/index','课程订单','fa fa-shopping-cart','course_order/index',1,186,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'baby/index','宝宝档案','fa fa-child','baby/index',1,185,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'growthrecord/index','成长记录','fa fa-line-chart','growth_record/index',1,184,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'vaccine/index','疫苗与儿保','fa fa-medkit','vaccine/index',1,183,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'counselor/index','咨询师','fa fa-user-md','counselor/index',1,182,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'consultorder/index','咨询订单','fa fa-comments','consult_order/index',1,181,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'assessment/index','量表管理','fa fa-clipboard','assessment/index',1,180,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'assessmentrecord/index','测评记录','fa fa-bar-chart','assessment_record/index',1,179,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'memberplan/index','会员套餐','fa fa-vimeo','member_plan/index',1,178,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'memberorder/index','会员订单','fa fa-credit-card','member_order/index',1,177,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'usersetting/index','隐私与提醒','fa fa-lock','user_setting/index',1,176,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'userbackup/index','数据备份','fa fa-database','user_backup/index',1,175,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP());
INSERT INTO `fa_auth_rule` (`type`,`pid`,`name`,`title`,`icon`,`url`,`ismenu`,`weigh`,`status`,`createtime`,`updatetime`) VALUES
('file',@parent,'manage/index','通用管理-index','fa fa-circle-o','',0,90,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'manage/add','通用管理-add','fa fa-circle-o','',0,89,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'manage/edit','通用管理-edit','fa fa-circle-o','',0,88,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'manage/del','通用管理-del','fa fa-circle-o','',0,87,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'manage/multi','通用管理-multi','fa fa-circle-o','',0,86,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()),
('file',@parent,'manage/columns','通用管理-columns','fa fa-circle-o','',0,85,'normal',UNIX_TIMESTAMP(),UNIX_TIMESTAMP());
-- 后台站点名称(登录页/侧边栏品牌)
UPDATE `fa_config` SET `value` = '花期运营中心' WHERE `name` = 'name' LIMIT 1;

查看文件

@@ -0,0 +1,89 @@
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
var Controller = {
index: function () {
var table = $("#table");
var tableName = table.data("table");
var meta = Config.columns || [];
// 依据后端字段元数据动态构建列
var head = [[{checkbox: true, width: 40}]];
meta.forEach(function (col) {
head[0].push({
field: col.field,
title: col.title,
sortable: !!col.sortable,
operate: col.search ? 'LIKE' : false,
formatter: function (value) {
if (value === null || value === undefined || value === '' || value === '0' && col.type === 'date') {
return '<span class="text-muted">-</span>';
}
if (value === '') {
return '<span class="text-muted">-</span>';
}
return $('<div>').text(value).html();
}
});
});
head[0].push({
field: 'operate',
title: '操作',
table: table,
events: Table.api.events.operate,
formatter: Table.api.formatter.operate,
width: 110
});
Table.api.init({
extend: {
index_url: 'manage/index?table=' + tableName,
add_url: 'manage/add?table=' + tableName,
edit_url: 'manage/edit?table=' + tableName,
del_url: 'manage/del?table=' + tableName,
multi_url: 'manage/multi?table=' + tableName,
table: tableName
}
});
table.bootstrapTable({
url: 'manage/index?table=' + tableName,
pk: 'id',
sortName: 'id',
sortOrder: 'desc',
search: true,
commonSearch: false,
showExport: false,
pageSize: 20,
pageList: [10, 20, 50, 100],
columns: head,
queryParams: function (params) {
params.table = tableName;
return params;
}
});
Table.api.bindevent(table);
table.on('load-success.bs.table', function (e, data) {
if (data && typeof data.total !== 'undefined') {
$('#hq-total').text(data.total);
}
});
},
add: function () {
Controller.api.bindevent();
},
edit: function () {
Controller.api.bindevent();
},
api: {
bindevent: function () {
Form.api.bindevent($("form[role=form]"));
$(document).on('click', '.btn-return', function () {
parent.Layer.closeAll();
});
}
}
};
return Controller;
});