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
这个提交包含在:
7
.gitignore
vendored
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
|
||||
|
||||
135
docs/06-运营后台与数据同步.md
普通文件
135
docs/06-运营后台与数据同步.md
普通文件
@@ -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;
|
||||
}
|
||||
|
||||
.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>
|
||||
<!--@formatter:off-->
|
||||
{if $background}
|
||||
<style type="text/css">
|
||||
body{
|
||||
background-image: url('{$background|htmlentities}');
|
||||
}
|
||||
</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="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>
|
||||
<div class="hq-brand-name">花期</div>
|
||||
<div class="hq-brand-slogan">温柔记录 · 静待花开</div>
|
||||
</div>
|
||||
<p class="hq-brand-desc">
|
||||
运营管理后台<br>
|
||||
内容审核 · 用户运营 · 数据看板
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form action="" method="post" id="login-form">
|
||||
<!--@AdminLoginFormBegin-->
|
||||
<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>
|
||||
<!-- 表单区 -->
|
||||
<div class="hq-form-panel">
|
||||
<h1 class="hq-form-title">登录运营后台</h1>
|
||||
<p class="hq-form-sub">请使用管理员账号登录,所有操作将被记录</p>
|
||||
|
||||
<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>
|
||||
<!--@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();"/>
|
||||
</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)}">
|
||||
<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-->
|
||||
</form>
|
||||
<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="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="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>
|
||||
|
||||
{if $Think.config.fastadmin.login_captcha}
|
||||
<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}
|
||||
|
||||
{if $keeyloginhours>0}
|
||||
<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>
|
||||
{/if}
|
||||
|
||||
<button type="submit" class="hq-submit">{:__('Sign in')}</button>
|
||||
</form>
|
||||
|
||||
<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
普通文件
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;
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户