文档修改
This commit is contained in:
@@ -762,15 +762,64 @@ class PropertyManager(ActiveManager):
|
||||
## 八、数据量与性能预测
|
||||
|
||||
| 表名 | 预估行数 | 增长速度 | 分区策略 |
|
||||
|------|---------|---------|---------|
|
||||
|------|---------|---------|---------|
|
||||
| `properties` | 89,000+ | 中速 | 暂不分区,建议 500k 后按 `created_at` RANGE 分区 |
|
||||
| `follow_logs` | 200万+ | 高速(最高频写入) | 按 `created_at` 月度 RANGE 分区 |
|
||||
| `property_photos` | 500万+ | 高速 | 按 `property_id` HASH 分区(16分区) |
|
||||
| `follow_logs` | 200万+ | 高速(最高频写入) | ✅ `PARTITION BY RANGE (created_at)` 月度分区 |
|
||||
| `property_photos` | 500万+ | 高速 | ✅ `PARTITION BY RANGE (created_at)` 月度分区 |
|
||||
| `permission_change_logs` | 100万+ | 中高速 | ✅ `PARTITION BY RANGE (operated_at)` 月度分区 |
|
||||
| `login_attempts` | 500万+ | 高速(每次登录一条) | ✅ `PARTITION BY RANGE (attempted_at)` 月度分区 |
|
||||
| `platform_audit_logs` | 10万+ | 低中速 | ✅ `PARTITION BY RANGE (created_at)` 月度分区 |
|
||||
| `price_changes` | 50万 | 中速 | 无需分区 |
|
||||
| `listing_histories` | 20万 | 低速 | 无需分区 |
|
||||
| `clients` | 10万+ | 中速 | 暂不分区 |
|
||||
| `viewings` | 100万 | 中速 | 无需分区 |
|
||||
|
||||
### 8.1 分区维护策略(partition_maintenance_task)
|
||||
|
||||
所有月度分区表统一由 **Celery Beat 定时任务** `partition_maintenance_task` 维护,每月 1 日凌晨 01:00(UTC+8)自动执行:
|
||||
|
||||
```python
|
||||
# apps/property/tasks.py(及 permission/login/shared 各 App 对应任务)
|
||||
@app.task(name="partition_maintenance_task")
|
||||
def partition_maintenance_task():
|
||||
"""
|
||||
为下一个月预建所有分区表的分区。
|
||||
- 检查是否已存在目标分区,幂等执行
|
||||
- 失败时发送 Sentry 告警
|
||||
"""
|
||||
tables = [
|
||||
("follow_logs", "created_at"),
|
||||
("property_photos", "created_at"),
|
||||
("permission_change_logs", "operated_at"),
|
||||
("login_attempts", "attempted_at"),
|
||||
("public.platform_audit_logs", "created_at"),
|
||||
]
|
||||
next_month = date.today().replace(day=1) + relativedelta(months=1)
|
||||
month_start = next_month
|
||||
month_end = next_month + relativedelta(months=1)
|
||||
|
||||
for table, _key in tables:
|
||||
suffix = month_start.strftime("%Y_%m")
|
||||
part_name = f"{table.replace('.', '_')}_{suffix}"
|
||||
sql = f"""
|
||||
CREATE TABLE IF NOT EXISTS {part_name}
|
||||
PARTITION OF {table}
|
||||
FOR VALUES FROM ('{month_start}') TO ('{month_end}');
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql)
|
||||
```
|
||||
|
||||
**Celery Beat 配置**(`celery.py`):
|
||||
```python
|
||||
app.conf.beat_schedule["partition_maintenance_task"] = {
|
||||
"task": "partition_maintenance_task",
|
||||
"schedule": crontab(day_of_month=1, hour=1, minute=0), # 每月1日 01:00 UTC+8
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ **注意**:每张分区表均保留一个 `_default` 默认分区作为兜底,防止任务失败时写入报错。`_default` 分区数据应在运维 SOP 中周期性检查(有数据则说明提前建分区失败)。
|
||||
|
||||
---
|
||||
|
||||
## 九、必须在开发启动前明确的数据架构决策
|
||||
|
||||
@@ -107,6 +107,7 @@ Staff (员工)
|
||||
| deleted_at | TIMESTAMPTZ | | 软删除 |
|
||||
| created_by | UUID | FK→staff, SET NULL | |
|
||||
| updated_by | UUID | FK→staff, SET NULL | |
|
||||
| version | INTEGER | NOT NULL DEFAULT 1 | 乐观锁版本号;每次 UPDATE +1;应用层检测 0 行受影响时抛 ConflictError |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
|
||||
@@ -204,6 +204,7 @@ CREATE INDEX idx_schools_name_trgm ON schools USING gin(name gin_trgm_ops);
|
||||
| deleted_at | TIMESTAMPTZ | | 软删除 |
|
||||
| created_by | UUID | FK→staff, SET NULL | |
|
||||
| updated_by | UUID | FK→staff, SET NULL | |
|
||||
| version | INTEGER | NOT NULL DEFAULT 1 | 乐观锁版本号;每次 UPDATE +1;应用层检测 0 行受影响时抛 ConflictError |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
|
||||
@@ -190,13 +190,39 @@ class UserAccount(AbstractBaseUser):
|
||||
|
||||
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| `id` | `BIGSERIAL` | `PRIMARY KEY` | — | 自增主键 |
|
||||
| `id` | `BIGSERIAL` | `NOT NULL` | — | 自增主键(与 attempted_at 组成复合 PK) |
|
||||
| `attempted_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 尝试时间(分区键,必须在复合主键中) |
|
||||
| `username` | `VARCHAR(30)` | `NOT NULL` | — | 尝试登录的用户名(冗余存储,即使账号不存在也记录) |
|
||||
| `ip_address` | `INET` | `NOT NULL` | — | 来源 IP 地址(支持 IPv4/IPv6) |
|
||||
| `user_agent` | `TEXT` | `NULL` | `NULL` | 客户端 User-Agent(Electron 版本信息) |
|
||||
| `success` | `BOOLEAN` | `NOT NULL` | — | 是否登录成功 |
|
||||
| `failure_reason` | `VARCHAR(30)` | `NULL` | `NULL` | 失败原因;可选值见下方枚举 |
|
||||
| `attempted_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 尝试时间 |
|
||||
|
||||
> ⚠️ **分区说明**:`login_attempts` 为高写入审计表,采用 `PARTITION BY RANGE (attempted_at)` 按月分区。主键为 `(id, attempted_at)` 复合主键(分区表规范:主键必须包含分区键)。
|
||||
|
||||
**DDL**:
|
||||
```sql
|
||||
CREATE TABLE login_attempts (
|
||||
id BIGSERIAL NOT NULL,
|
||||
attempted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 分区键
|
||||
username VARCHAR(30) NOT NULL,
|
||||
ip_address INET NOT NULL,
|
||||
user_agent TEXT,
|
||||
success BOOLEAN NOT NULL,
|
||||
failure_reason VARCHAR(30)
|
||||
CHECK (failure_reason IS NULL OR failure_reason IN (
|
||||
'wrong_password','wrong_captcha','account_locked',
|
||||
'account_disabled','tenant_not_found'
|
||||
)),
|
||||
PRIMARY KEY (id, attempted_at) -- 分区表主键必须包含分区键
|
||||
) PARTITION BY RANGE (attempted_at);
|
||||
|
||||
CREATE TABLE login_attempts_2026_04 PARTITION OF login_attempts
|
||||
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
|
||||
CREATE TABLE login_attempts_2026_05 PARTITION OF login_attempts
|
||||
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
|
||||
CREATE TABLE login_attempts_default PARTITION OF login_attempts DEFAULT;
|
||||
```
|
||||
|
||||
**`failure_reason` 枚举值**:
|
||||
|
||||
|
||||
@@ -988,7 +988,8 @@ CREATE INDEX idx_data_scopes_expires ON staff_data_scopes(expires_at) WHERE expi
|
||||
|
||||
-- permission_change_logs (append-only, no deleted_at)
|
||||
CREATE TABLE permission_change_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
operated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 分区键(原 operated_at 前置)
|
||||
target_type VARCHAR(30) NOT NULL
|
||||
CHECK (target_type IN ('role','role_permission','staff_role','staff_override','staff_scope')),
|
||||
target_id UUID NOT NULL,
|
||||
@@ -1003,8 +1004,15 @@ CREATE TABLE permission_change_logs (
|
||||
operator_ip INET,
|
||||
user_agent TEXT,
|
||||
reason TEXT NOT NULL DEFAULT '',
|
||||
operated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
PRIMARY KEY (id, operated_at) -- 分区表主键必须包含分区键
|
||||
) PARTITION BY RANGE (operated_at);
|
||||
|
||||
CREATE TABLE permission_change_logs_2026_04 PARTITION OF permission_change_logs
|
||||
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
|
||||
CREATE TABLE permission_change_logs_2026_05 PARTITION OF permission_change_logs
|
||||
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
|
||||
CREATE TABLE permission_change_logs_default PARTITION OF permission_change_logs DEFAULT;
|
||||
CREATE INDEX idx_perm_log_staff ON permission_change_logs(staff_id, operated_at DESC) WHERE staff_id IS NOT NULL;
|
||||
CREATE INDEX idx_perm_log_role ON permission_change_logs(role_id, operated_at DESC) WHERE role_id IS NOT NULL;
|
||||
CREATE INDEX idx_perm_log_target ON permission_change_logs(target_type, target_id, operated_at DESC);
|
||||
|
||||
@@ -310,7 +310,10 @@ CREATE TABLE properties (
|
||||
updated_by UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||||
|
||||
-- ── 全文检索向量 ──
|
||||
search_vector TSVECTOR
|
||||
search_vector TSVECTOR,
|
||||
|
||||
-- ── 乐观锁 ──
|
||||
version INTEGER NOT NULL DEFAULT 1 -- 每次 UPDATE 必须 +1;应用层检测 0 行受影响时抛 ConflictError
|
||||
);
|
||||
|
||||
-- ── 索引策略 ──
|
||||
@@ -520,7 +523,8 @@ CREATE INDEX idx_price_changes_time ON price_changes(property_id, changed_at DES
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE follow_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 分区键,必须在最前声明
|
||||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||||
|
||||
log_type VARCHAR(30) NOT NULL
|
||||
@@ -554,9 +558,19 @@ CREATE TABLE follow_logs (
|
||||
-- 是否可删除(sensitive_view = FALSE,合规强制)
|
||||
is_deletable BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ -- 仅 is_deletable=TRUE 时可软删
|
||||
);
|
||||
deleted_at TIMESTAMPTZ, -- 仅 is_deletable=TRUE 时可软删
|
||||
|
||||
PRIMARY KEY (id, created_at) -- 分区表主键必须包含分区键
|
||||
) PARTITION BY RANGE (created_at);
|
||||
|
||||
-- ── 按月自动建分区(由 partition_maintenance_task Celery 任务维护)──
|
||||
-- 示例:初始建立当前月 + 下一个月的分区
|
||||
CREATE TABLE follow_logs_2026_04 PARTITION OF follow_logs
|
||||
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
|
||||
CREATE TABLE follow_logs_2026_05 PARTITION OF follow_logs
|
||||
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
|
||||
-- 默认分区:兜底,防止超出已建分区范围导致写入失败
|
||||
CREATE TABLE follow_logs_default PARTITION OF follow_logs DEFAULT;
|
||||
|
||||
-- 时间线展示(核心)
|
||||
CREATE INDEX idx_follow_logs_property_time ON follow_logs(property_id, created_at DESC)
|
||||
@@ -798,7 +812,8 @@ CREATE INDEX idx_survey_photos_category ON survey_photos(survey_id, category);
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE property_photos (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 分区键
|
||||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||||
|
||||
category VARCHAR(20) NOT NULL
|
||||
@@ -817,10 +832,17 @@ CREATE TABLE property_photos (
|
||||
is_cover BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
sort_order SMALLINT NOT NULL DEFAULT 0,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
||||
);
|
||||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||||
|
||||
PRIMARY KEY (id, created_at) -- 分区表主键必须包含分区键
|
||||
) PARTITION BY RANGE (created_at);
|
||||
|
||||
CREATE TABLE property_photos_2026_04 PARTITION OF property_photos
|
||||
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
|
||||
CREATE TABLE property_photos_2026_05 PARTITION OF property_photos
|
||||
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
|
||||
CREATE TABLE property_photos_default PARTITION OF property_photos DEFAULT;
|
||||
|
||||
CREATE INDEX idx_property_photos_property ON property_photos(property_id);
|
||||
CREATE INDEX idx_property_photos_cover ON property_photos(property_id)
|
||||
|
||||
@@ -241,7 +241,8 @@ CREATE INDEX idx_ip_whitelist_active ON public.ip_whitelist(cidr) WHERE is_activ
|
||||
|
||||
-- 平台操作审计日志(所有写操作 + 高危操作,无 deleted_at,无 UPDATE)
|
||||
CREATE TABLE public.platform_audit_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 分区键
|
||||
operator_id UUID, -- 管理员 ID;NULL 表示系统自动操作
|
||||
operator_name VARCHAR(100), -- 快照(防止账号删除后失去溯源)
|
||||
action_type VARCHAR(50) NOT NULL,
|
||||
@@ -257,9 +258,16 @@ CREATE TABLE public.platform_audit_logs (
|
||||
CHECK (result IN ('SUCCESS','FAILED')),
|
||||
error_message TEXT,
|
||||
ip_address INET,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
-- 无 deleted_at,无 UPDATE;建议按月 RANGE 分区
|
||||
);
|
||||
-- 无 deleted_at,无 UPDATE;按月 RANGE 分区
|
||||
|
||||
PRIMARY KEY (id, created_at) -- 分区表主键必须包含分区键
|
||||
) PARTITION BY RANGE (created_at);
|
||||
|
||||
CREATE TABLE public.platform_audit_logs_2026_04 PARTITION OF public.platform_audit_logs
|
||||
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
|
||||
CREATE TABLE public.platform_audit_logs_2026_05 PARTITION OF public.platform_audit_logs
|
||||
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
|
||||
CREATE TABLE public.platform_audit_logs_default PARTITION OF public.platform_audit_logs DEFAULT;
|
||||
|
||||
CREATE INDEX idx_audit_logs_operator ON public.platform_audit_logs(operator_id, created_at DESC);
|
||||
CREATE INDEX idx_audit_logs_action ON public.platform_audit_logs(action_type, created_at DESC);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -230,6 +230,8 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - 账号密码登录
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/登录管理/登录_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/登录_账号密码_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/登录_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/登录管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -247,6 +249,8 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - 账号密码登录
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/登录管理/登录_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/登录_账号密码_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/登录_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/登录管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -305,7 +309,8 @@
|
||||
**引用文档**
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - 多租户识别
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`
|
||||
- UI:N/A
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/登录_账号密码_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/登录_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/登录管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -322,6 +327,8 @@
|
||||
【输入文档(必须阅读)】
|
||||
- 参考PRD文档:`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - 多租户识别
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/登录_账号密码_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/登录_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/登录管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -380,7 +387,8 @@
|
||||
**引用文档**
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - Token 管理/会话超时
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`
|
||||
- UI:N/A
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/登录_账号密码_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/登录_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/登录管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -397,6 +405,8 @@
|
||||
【输入文档(必须阅读)】
|
||||
- 参考PRD文档:`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - Token 管理/会话超时
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/登录_账号密码_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/登录_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/登录管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1293,6 +1303,9 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 楼盘信息管理
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/楼盘管理/楼盘详情_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/区域管理_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/楼盘列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/楼盘详情_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/楼盘管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1310,6 +1323,9 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 楼盘信息管理
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/楼盘管理/楼盘详情_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/区域管理_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/楼盘列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/楼盘详情_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/楼盘管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1373,6 +1389,9 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 楼盘列表/楼盘详情
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/楼盘管理/楼盘列表_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/区域管理_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/楼盘列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/楼盘详情_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/楼盘管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1390,6 +1409,9 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 楼盘列表/楼盘详情
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/楼盘管理/楼盘列表_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/区域管理_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/楼盘列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/楼盘详情_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/楼盘管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1448,7 +1470,9 @@
|
||||
**引用文档**
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 区域管理
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md`
|
||||
- UI:N/A
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/区域管理_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/楼盘列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/楼盘详情_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/楼盘管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1465,6 +1489,9 @@
|
||||
【输入文档(必须阅读)】
|
||||
- 参考PRD文档:`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 区域管理
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/区域管理_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/楼盘列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/楼盘详情_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/楼盘管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1524,6 +1551,9 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 录入住宅(二手出售/出租)
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/房源管理/新增房源_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1541,6 +1571,9 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 录入住宅(二手出售/出租)
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/房源管理/新增房源_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1600,6 +1633,9 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 房源列表(二手&租赁)
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/房源管理/房源列表_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1617,6 +1653,9 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 房源列表(二手&租赁)
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/房源管理/房源列表_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1676,6 +1715,9 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 房源详情页
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/房源管理/房源详情_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1693,6 +1735,9 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 房源详情页
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/房源管理/房源详情_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1751,7 +1796,9 @@
|
||||
**引用文档**
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 跟进记录(全部/写入/修改/其他)
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- UI:N/A
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1768,6 +1815,9 @@
|
||||
【输入文档(必须阅读)】
|
||||
- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 跟进记录(全部/写入/修改/其他)
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1826,7 +1876,9 @@
|
||||
**引用文档**
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 图片管理(相册上传/分类/排序)
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- UI:N/A
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1843,6 +1895,9 @@
|
||||
【输入文档(必须阅读)】
|
||||
- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 图片管理(相册上传/分类/排序)
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1901,7 +1956,9 @@
|
||||
**引用文档**
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 业主联系人管理
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- UI:N/A
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1918,6 +1975,9 @@
|
||||
【输入文档(必须阅读)】
|
||||
- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 业主联系人管理
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1976,7 +2036,9 @@
|
||||
**引用文档**
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 价格调整(调价/调价记录)
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- UI:N/A
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -1993,6 +2055,9 @@
|
||||
【输入文档(必须阅读)】
|
||||
- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 价格调整(调价/调价记录)
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2051,7 +2116,9 @@
|
||||
**引用文档**
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 房源状态变更(在售/暂缓/成交/下架)
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- UI:N/A
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2068,6 +2135,9 @@
|
||||
【输入文档(必须阅读)】
|
||||
- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 房源状态变更(在售/暂缓/成交/下架)
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增房源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/房源详情_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/房源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2127,6 +2197,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 1:经纪人录入新私客;5.2 录入私客
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/新增客源_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2144,6 +2218,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 1:经纪人录入新私客;5.2 录入私客
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/新增客源_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2203,6 +2281,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 2/3/4:经纪人查看与筛选私客列表;5.1 客源列表
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源列表_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2220,6 +2302,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 2/3/4:经纪人查看与筛选私客列表;5.1 客源列表
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源列表_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2279,6 +2365,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 2:列表批量操作;5.1.3 批量操作
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源列表_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2296,6 +2386,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 2:列表批量操作;5.1.3 批量操作
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源列表_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2355,6 +2449,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 6:经纪人查看私客详情页;Story 15:经纪人查看客源信息概览面板
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2372,6 +2470,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 6:经纪人查看私客详情页;Story 15:经纪人查看客源信息概览面板
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2431,6 +2533,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 7:经纪人查看与编辑需求信息
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2448,6 +2554,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 7:经纪人查看与编辑需求信息
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2507,6 +2617,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 8:经纪人写入与查看跟进记录
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2524,6 +2638,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 8:经纪人写入与查看跟进记录
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2583,6 +2701,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 9:经纪人管理带看记录
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2600,6 +2722,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 9:经纪人管理带看记录
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2659,6 +2785,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 23:经纪人管理客源联系人
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2676,6 +2806,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 23:经纪人管理客源联系人
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2735,6 +2869,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 17:经纪人修改客源等级
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2752,6 +2890,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 17:经纪人修改客源等级
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2811,6 +2953,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 18:经纪人修改客源状态
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2828,6 +2974,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 18:经纪人修改客源状态
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2887,6 +3037,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 19:经纪人手动将私客转为公客
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2904,6 +3058,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 19:经纪人手动将私客转为公客
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2963,6 +3121,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 20:经纪人将私客转为成交客
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -2980,6 +3142,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 20:经纪人将私客转为成交客
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -3039,6 +3205,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 21:经纪人将客源标记为无效
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -3056,6 +3226,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 21:经纪人将客源标记为无效
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -3115,6 +3289,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 14:经纪人编辑客源信息
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/编辑客源_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -3132,6 +3310,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 14:经纪人编辑客源信息
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/编辑客源_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -3191,6 +3373,10 @@
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 24:经纪人管理客源相关员工
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- - 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -3208,6 +3394,10 @@
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 24:经纪人管理客源相关员工
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -3266,7 +3456,10 @@
|
||||
**引用文档**
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - 关键业务规则:私客自动转公
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- UI:N/A
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -3283,6 +3476,10 @@
|
||||
【输入文档(必须阅读)】
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - 关键业务规则:私客自动转公
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -3341,7 +3538,10 @@ Celery Beat 定时任务每日凌晨执行;超过运营配置天数(如30天
|
||||
**引用文档**
|
||||
- - 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 2:顶部重复检测提示;关键业务规则:私客手机号唯一性
|
||||
- - 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- UI:N/A
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- - 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- - 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
@@ -3358,6 +3558,10 @@ Celery Beat 定时任务每日凌晨执行;超过运营配置天数(如30天
|
||||
【输入文档(必须阅读)】
|
||||
- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 2:顶部重复检测提示;关键业务规则:私客手机号唯一性
|
||||
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- 参考UI静态页面:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/客源管理技术方案.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
- 参考TECH_STACK文档:`Project/fonrey/TECH_STACK/测试规范.md`
|
||||
|
||||
@@ -31,18 +31,18 @@
|
||||
|
||||
### 核心问题摘录(Top 10)
|
||||
|
||||
| # | 等级 | 编号 | 问题 | 维度 | 状态 |
|
||||
|---|------|------|------|------|------|
|
||||
| 1 | 🔴 | **B-05** | 主表乐观锁 `version` 字段全量 0 实现:`properties` / `clients` / `complexes` 在 PRD 多人协作场景中是核心,DDL 无并发控制 | Data↔PRD | 🆕 升 Blocker(持续 3 次未修) |
|
||||
| 2 | 🔴 | **B-06** | 高写入表分区 DDL 仍未落地(M-03 持续 3 次未修):5 张高频表无 `PARTITION BY RANGE` 子句,仅注释"建议月度分区",无法在迁移期补加分区 | Data | 🆕 升 Blocker |
|
||||
| 3 | 🟠 | **M-11** | KMS / 密钥轮换 SOP 仍未补:`core/encryption.py` 已声明,但主密钥轮换、密钥版本号、加密字段重新封装、应急吊销四类流程在 TECH_STACK 与系统管理 PRD 中均无对应章节 | 安全 | ❌ 持续未修 |
|
||||
| 4 | 🟠 | **M-12** | Celery 任务 schema 切换缺统一封装:多模块技术方案声明 `tenant_schema_name` 入参,但无 `with_tenant_context` 装饰器或基类抽象,开发期容易漏写导致跨租户脏读 | TECH/多租户 | ⚠️ 部分修复 |
|
||||
| 5 | 🟠 | **M-13** | R2 路径前缀全局规范不一致:系统管理已规范 `backups/{tenant_schema}/...` `exports/{tenant_schema}/...`,但客源/房源/楼盘模块 R2 路径仍写"`property_photos/...`"无 tenant 前缀模板 | TECH/多租户 | ⚠️ 部分修复 |
|
||||
| 6 | 🟠 | **M-06** | 客户端发布无签名校验/防降级:`/api/client/updates/latest/` 与 `download_url` 对外公开,仅 SHA256 完整性校验,可被 MITM 投递降级版本(昨日 M-06 未修) | 安全 | ❌ 持续未修 |
|
||||
| 7 | 🟠 | **M-14** | ORM Manager / QuerySet 数据范围统一封装规范缺失:`DATA_MODEL_PERMISSION.md:143-145` 的 `ScopeQueryBuilder` 只是 helper,未规定"所有业务 QuerySet 必须经过 Scope 包装"的强制约束,开发期容易漏权限校验 | 安全/Data | 🆕 新增 |
|
||||
| 8 | 🟠 | **M-05** | 89k 数据 < 2 秒列表查询 NFR 仍无 p95/EXPLAIN/性能基准测试任务(昨日 M-05 未修) | NFR↔TECH↔测试 | ❌ 持续未修 |
|
||||
| 9 | 🟠 | **M-09** | UI_SYSTEM 复杂组件(虚拟滚动列表、批量操作面板、抽屉表单嵌套规则、文件上传批量、富权限树)规范深度不足;UI_DESIGN 11 份原型仅覆盖客源 + 房源列表,**楼盘/权限/系统配置/组织人事/发布管理 5 大模块全部缺原型** | UI | ⚠️ 部分修复 |
|
||||
| 10 | 🟡 | **N-01** | ENUMS.md v2.2 已统一,但 PRD 文本中仍混用中文枚举(如客源 PRD 仍写"求购/求租"),需要一次全文档"中文枚举 → ENUMS 锚点链接"替换 | PRD↔Data | 🆕 新增 |
|
||||
| # | 等级 | 编号 | 问题 | 维度 | 状态 |
|
||||
| --- | --- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ---------------------- |
|
||||
| 1 | 🔴 | **B-05** | 主表乐观锁 `version` 字段全量 0 实现:`properties` / `clients` / `complexes` 在 PRD 多人协作场景中是核心,DDL 无并发控制 | Data↔PRD | 🆕 升 Blocker(持续 3 次未修) |
|
||||
| 2 | 🔴 | **B-06** | 高写入表分区 DDL 仍未落地(M-03 持续 3 次未修):5 张高频表无 `PARTITION BY RANGE` 子句,仅注释"建议月度分区",无法在迁移期补加分区 | Data | 🆕 升 Blocker |
|
||||
| 3 | 🟠 | **M-11** | KMS / 密钥轮换 SOP 仍未补:`core/encryption.py` 已声明,但主密钥轮换、密钥版本号、加密字段重新封装、应急吊销四类流程在 TECH_STACK 与系统管理 PRD 中均无对应章节 | 安全 | ❌ 持续未修 |
|
||||
| 4 | 🟠 | **M-12** | Celery 任务 schema 切换缺统一封装:多模块技术方案声明 `tenant_schema_name` 入参,但无 `with_tenant_context` 装饰器或基类抽象,开发期容易漏写导致跨租户脏读 | TECH/多租户 | ⚠️ 部分修复 |
|
||||
| 5 | 🟠 | **M-13** | R2 路径前缀全局规范不一致:系统管理已规范 `backups/{tenant_schema}/...` `exports/{tenant_schema}/...`,但客源/房源/楼盘模块 R2 路径仍写"`property_photos/...`"无 tenant 前缀模板 | TECH/多租户 | ⚠️ 部分修复 |
|
||||
| 6 | 🟠 | **M-06** | 客户端发布无签名校验/防降级:`/api/client/updates/latest/` 与 `download_url` 对外公开,仅 SHA256 完整性校验,可被 MITM 投递降级版本(昨日 M-06 未修) | 安全 | ❌ 持续未修 |
|
||||
| 7 | 🟠 | **M-14** | ORM Manager / QuerySet 数据范围统一封装规范缺失:`DATA_MODEL_PERMISSION.md:143-145` 的 `ScopeQueryBuilder` 只是 helper,未规定"所有业务 QuerySet 必须经过 Scope 包装"的强制约束,开发期容易漏权限校验 | 安全/Data | 🆕 新增 |
|
||||
| 8 | 🟠 | **M-05** | 89k 数据 < 2 秒列表查询 NFR 仍无 p95/EXPLAIN/性能基准测试任务(昨日 M-05 未修) | NFR↔TECH↔测试 | ❌ 持续未修 |
|
||||
| 9 | 🟠 | **M-09** | UI_SYSTEM 复杂组件(虚拟滚动列表、批量操作面板、抽屉表单嵌套规则、文件上传批量、富权限树)规范深度不足;UI_DESIGN 11 份原型仅覆盖客源 + 房源列表,**楼盘/权限/系统配置/组织人事/发布管理 5 大模块全部缺原型** | UI | ⚠️ 部分修复 |
|
||||
| 10 | 🟡 | **N-01** | ENUMS.md v2.2 已统一,但 PRD 文本中仍混用中文枚举(如客源 PRD 仍写"求购/求租"),需要一次全文档"中文枚举 → ENUMS 锚点链接"替换 | PRD↔Data | 🆕 新增 |
|
||||
|
||||
### 风险等级分布
|
||||
|
||||
|
||||
@@ -267,7 +267,79 @@ Fonrey 优先采用“预签名上传 + 回执提交(commit)”两段式。
|
||||
|
||||
---
|
||||
|
||||
## 9. 与模块文档的衔接规则
|
||||
## 9. 乐观锁(Optimistic Locking)规范
|
||||
|
||||
### 9.1 适用场景
|
||||
|
||||
`properties`、`clients`、`complexes` 三张高竞争表的更新操作(`PUT`/`PATCH`)**MUST** 使用乐观锁并发控制,防止"后写覆盖先写"数据丢失。
|
||||
|
||||
### 9.2 请求规范
|
||||
|
||||
客户端发起更新时,MUST 在请求体中携带当前资源版本号:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"sale_price": 180,
|
||||
"version": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 兼容说明:当前 Fonrey 为内部 Web / Electron 客户端,采用请求体传递 `version` 字段,无需 `If-Match` Header(避免 HTMX 额外配置复杂度)。未来若提供对外开放 REST API,可补充支持 `If-Match: <version>` Header 形式。
|
||||
|
||||
### 9.3 服务端执行规范
|
||||
|
||||
服务端执行 UPDATE 时 MUST 同时匹配 `version`,并将 `version` +1:
|
||||
|
||||
```sql
|
||||
UPDATE properties
|
||||
SET sale_price = :sale_price,
|
||||
version = version + 1,
|
||||
updated_at = NOW(),
|
||||
updated_by = :operator_id
|
||||
WHERE id = :id
|
||||
AND version = :client_version -- 乐观锁匹配
|
||||
AND deleted_at IS NULL;
|
||||
```
|
||||
|
||||
- 若受影响行数 **= 1**:更新成功,返回 `200`
|
||||
- 若受影响行数 **= 0**:抛 `ConflictError`,返回 `409` + code `*_VERSION_CONFLICT`
|
||||
|
||||
### 9.4 冲突响应规范
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": "已被他人修改,请刷新重试",
|
||||
"code": "PROPERTY_VERSION_CONFLICT",
|
||||
"details": {
|
||||
"field": "version",
|
||||
"your_version": 3,
|
||||
"hint": "请重新获取最新数据后再提交"
|
||||
},
|
||||
"meta": {
|
||||
"request_id": "uuid",
|
||||
"timestamp": "2026-04-28T10:00:00+08:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- HTTP 状态码 MUST 为 `409`
|
||||
- `code` 格式:`<MODULE>_VERSION_CONFLICT`(如 `PROPERTY_VERSION_CONFLICT` / `CLIENT_VERSION_CONFLICT` / `COMPLEX_VERSION_CONFLICT`)
|
||||
- 前端 SHOULD 展示 Toast:**「已被他人修改,请刷新重试」**,并自动触发资源重新加载
|
||||
|
||||
### 9.5 Check List
|
||||
|
||||
- [ ] `version` 字段在 GET 响应中 MUST 返回(供后续 PUT/PATCH 携带)
|
||||
- [ ] 服务层 update 方法 MUST 校验受影响行数,0 行时抛 `ConflictError`
|
||||
- [ ] 前端表单 MUST 在隐藏域中保存 `version`,随 PUT/PATCH 提交
|
||||
- [ ] HTMX 场景:冲突时后端 MUST 返回 `HX-Trigger: {"toast:error":"已被他人修改,请刷新重试"}`
|
||||
- [ ] 测试 MUST 覆盖:并发两次更新同版本,第二次 MUST 返回 `409`
|
||||
|
||||
---
|
||||
|
||||
## 10. 与模块文档的衔接规则
|
||||
|
||||
- 各模块技术方案中的“四、API 设计原则”“六、关键 API 规范”“十二、错误码建议”必须引用本文件
|
||||
- 模块文档可补充模块特有 code 与字段,但不得与本规范冲突
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
|
||||
|
||||
**版本**: 2.2 | **最后更新**: 2026-04-27
|
||||
**版本**: 2.3 | **最后更新**: 2026-04-29
|
||||
**定位**: 本文档是 Fonrey 项目技术栈的**总索引**。所有跨模块的技术决策、版本约束、目录规范、禁止项在此定稿;**单一模块的具体技术方案**(数据模型、服务层、HTMX 交互、Celery 任务等)见各自子文档(见 §9 索引)。
|
||||
|
||||
---
|
||||
@@ -94,6 +94,9 @@ apps/property/
|
||||
- ❌ 客户端内嵌业务逻辑或本地数据库(壳应用原则)
|
||||
- ❌ 跨租户 SQL 查询(必须经 `django-tenants` 中间件切换 Schema)
|
||||
- ❌ 在代码中硬编码密钥、Tenant ID、URL
|
||||
- ❌ Celery 任务内手写 `connection.set_schema(...)`(必须用 `@tenant_task` 装饰器,见 §12)
|
||||
- ❌ 业务视图/服务层直接调用 `<Model>.objects.filter/get/all(...)`(必须用 `Model.scoped(staff)`,见 §14)
|
||||
- ❌ R2 对象 key 使用原始文件名或 tenant_id(UUID)前缀(必须按 §13 路径模板)
|
||||
|
||||
---
|
||||
|
||||
@@ -238,4 +241,212 @@ Fonrey 采用 AI vibe coding 模式开发,测试是保证每日迭代质量的
|
||||
- 测试规范变更须同步更新 §10 关键结论,完整细节在 [`测试规范.md`](./测试规范.md) 中维护
|
||||
- 15 章节统一模板发生变更时,须先更新 §9 标准章节骨架,再同步各模块文档
|
||||
|
||||
---
|
||||
|
||||
## 12. Celery 多租户规范(M-12)
|
||||
|
||||
> **For AI assistants**: Every Celery task that touches tenant data MUST use the `@tenant_task` decorator defined here. No exceptions.
|
||||
|
||||
### 12.1 背景与风险
|
||||
|
||||
多模块技术方案均声明 Celery 任务签名带 `tenant_schema_name: str`,但 **缺乏统一封装**。
|
||||
Celery worker 复用进程池,相邻任务若未正确切换 `search_path`,会产生 **跨租户脏读/脏写**,且不报错。
|
||||
|
||||
### 12.2 `@tenant_task` 装饰器规范
|
||||
|
||||
位置:`core/celery_utils.py`(由架构师统一提供,禁止各模块自己实现)
|
||||
|
||||
```python
|
||||
# core/celery_utils.py
|
||||
import functools
|
||||
import structlog
|
||||
from django_tenants.utils import schema_context
|
||||
from celery import current_task
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
def tenant_task(schema_arg: str = "tenant_schema_name"):
|
||||
"""
|
||||
装饰器:在 Celery 任务执行前自动切换租户 Schema,结束/异常后还原为 public。
|
||||
|
||||
使用方式:
|
||||
@shared_task
|
||||
@tenant_task(schema_arg="tenant_schema_name")
|
||||
def export_properties(tenant_schema_name: str, ...):
|
||||
... # 此处 search_path 已切换到目标 schema
|
||||
"""
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
schema = kwargs.get(schema_arg)
|
||||
if not schema:
|
||||
# 位置参数兼容:尝试从 args 中读取(按函数签名顺序)
|
||||
import inspect
|
||||
sig = inspect.signature(func)
|
||||
params = list(sig.parameters.keys())
|
||||
if schema_arg in params:
|
||||
idx = params.index(schema_arg)
|
||||
schema = args[idx] if idx < len(args) else None
|
||||
if not schema:
|
||||
raise ValueError(
|
||||
f"[tenant_task] '{schema_arg}' 参数缺失,任务 {func.__name__} 拒绝执行"
|
||||
)
|
||||
log = logger.bind(task=func.__name__, task_id=current_task.request.id, schema=schema)
|
||||
log.info("tenant_task.start")
|
||||
try:
|
||||
with schema_context(schema):
|
||||
result = func(*args, **kwargs)
|
||||
log.info("tenant_task.success")
|
||||
return result
|
||||
except Exception as exc:
|
||||
log.error("tenant_task.error", exc=str(exc))
|
||||
raise
|
||||
return wrapper
|
||||
return decorator
|
||||
```
|
||||
|
||||
### 12.3 强制约束
|
||||
|
||||
| 约束 | 说明 |
|
||||
|---|---|
|
||||
| **所有业务 Celery 任务** 必须使用 `@tenant_task` | 包括导出、图片处理、智能配房、分区维护任务 |
|
||||
| 任务签名 **必须含 `tenant_schema_name: str`** 形参 | 位置或关键字均可,`schema_arg` 参数可自定义名称 |
|
||||
| 装饰器顺序:先 `@shared_task`,后 `@tenant_task` | 确保 Celery 正常注册任务名 |
|
||||
| **禁止** 在任务内部手写 `connection.set_schema(...)` | 统一走装饰器,禁止散落手写 |
|
||||
| 平台级无租户任务(如 `partition_maintenance_task`)| 直接 `with schema_context(target_schema):` 循环,不需要此装饰器 |
|
||||
|
||||
### 12.4 测试补充规范
|
||||
|
||||
- Celery 任务测试(`CELERY_TASK_ALWAYS_EAGER = True`)**必须**断言 `schema_context` 被以目标 schema 调用
|
||||
- 可用 `unittest.mock.patch("core.celery_utils.schema_context")` 拦截,断言 `call_args`
|
||||
- 反例测试:传入空 `tenant_schema_name` 时,任务必须抛出 `ValueError`,不得静默执行
|
||||
|
||||
---
|
||||
|
||||
## 13. R2 对象存储路径规范(M-13)
|
||||
|
||||
> **For AI assistants**: All R2 object keys MUST follow the template table below. Never invent custom prefixes.
|
||||
|
||||
### 13.1 路径模板表
|
||||
|
||||
| 资源类型 | Key 模板 | 访问方式 | TTL / 生命周期 |
|
||||
|---|---|---|---|
|
||||
| **客户端发布包** | `releases/system/{version}/{filename}` | public-read | 永久(不自动删除) |
|
||||
| **租户备份** | `backups/{tenant_schema}/{record_id}.tar.gz` | signed URL only | 90 天自动删除 |
|
||||
| **租户导出** | `exports/{tenant_schema}/{task_id}.zip` | signed URL 24h | 7 天自动删除 |
|
||||
| **房源图片** | `media/{tenant_schema}/property/{property_id}/{photo_id}.{ext}` | signed URL | 永久(随物理删除清理) |
|
||||
| **跟进附件** | `media/{tenant_schema}/follow/{log_id}/{idx}.{ext}` | signed URL | 90 天自动删除 |
|
||||
| **客源附件** | `media/{tenant_schema}/client/{client_id}/{idx}.{ext}` | signed URL | 永久(随物理删除清理) |
|
||||
| **审计归档** | `exports/audit/{task_id}.csv` | signed URL only | 2 年(合规保留) |
|
||||
|
||||
### 13.2 关键约束
|
||||
|
||||
- 路径中 **禁止使用 `tenant_id`(UUID)**,统一用 `tenant_schema_name`(字符串),便于跨环境迁移与 bucket policy 配置
|
||||
- `{ext}` 统一小写(`jpg` / `png` / `webp`),禁止 `.JPG`
|
||||
- 文件名仅用 UUID / 整数索引,**禁止使用原始文件名**(防路径注入)
|
||||
- Signed URL 生成统一通过 `core/storage.py` 的 `generate_presigned_url(key, expires_in)` 封装
|
||||
|
||||
### 13.3 Bucket Policy 摘要
|
||||
|
||||
| Prefix | Policy | 说明 |
|
||||
|---|---|---|
|
||||
| `releases/system/` | public-read | 客户端更新包,CDN 加速 |
|
||||
| `media/` | 无公开读,仅 signed | 所有租户媒体文件必须签名访问 |
|
||||
| `backups/` | 无公开读,仅 signed | 备份包,严禁公开 |
|
||||
| `exports/` | 无公开读,仅 signed | 导出包,签名 URL 有效期按上表 |
|
||||
|
||||
### 13.4 生命周期规则(R2 Object Lifecycle)
|
||||
|
||||
在 Cloudflare R2 控制台/API 配置以下规则:
|
||||
|
||||
```
|
||||
Rule 1: backups/ prefix → Delete after 90 days
|
||||
Rule 2: exports/{tenant}/ → Delete after 7 days
|
||||
Rule 3: follow/ in media/ → Delete after 90 days (与 follow_logs 分区归档对齐)
|
||||
Rule 4: exports/audit/ → Delete after 730 days (2 年合规保留)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. ORM 数据范围强制规范(M-14)
|
||||
|
||||
> **For AI assistants**: Never use `Model.objects.filter(...)` directly in views or services. Always go through `scoped(staff)`.
|
||||
|
||||
### 14.1 背景
|
||||
|
||||
`DATA_MODEL/DATA_MODEL_PERMISSION.md` 已实现 `ScopeQueryBuilder`,但未强制约束使用入口。
|
||||
模块技术方案 view 示例直接使用 `Property.objects.filter(...)`,可绕过权限控制。
|
||||
|
||||
### 14.2 强制规范
|
||||
|
||||
**所有业务 Model 必须暴露 `scoped(staff)` 入口,隐藏 `objects` Manager。**
|
||||
|
||||
```python
|
||||
# core/models/scoped.py
|
||||
from django.db import models
|
||||
|
||||
class ScopedManager(models.Manager):
|
||||
"""
|
||||
业务 Model 统一 Manager。
|
||||
直接调用 Model.objects 将报错,强制使用 Model.scoped(staff)。
|
||||
"""
|
||||
def get_queryset(self):
|
||||
raise RuntimeError(
|
||||
f"[ScopedManager] 禁止直接调用 {self.model.__name__}.objects。"
|
||||
f"请使用 {self.model.__name__}.scoped(staff) 经权限范围过滤。"
|
||||
)
|
||||
|
||||
def scoped(self, staff):
|
||||
"""
|
||||
返回经 ScopeQueryBuilder 过滤后的 QuerySet。
|
||||
staff: 当前登录员工实例(含角色、org_unit、权限范围)
|
||||
"""
|
||||
from apps.permission.scope import ScopeQueryBuilder
|
||||
return ScopeQueryBuilder(staff).apply(super().get_queryset())
|
||||
|
||||
|
||||
class ScopedModel(models.Model):
|
||||
"""
|
||||
所有业务 Model 继承此基类(而非 models.Model)。
|
||||
"""
|
||||
objects = ScopedManager()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
```
|
||||
|
||||
### 14.3 豁免场景
|
||||
|
||||
| 场景 | 豁免条件 | 写法 |
|
||||
|---|---|---|
|
||||
| 系统/平台级操作(如分区维护) | 无租户身份,有明确运维场景 | `Model._default_manager.filter(...)` + 代码注释说明 |
|
||||
| 迁移脚本 / seed factory | `RunPython` 或测试工厂 | `Model._default_manager.all()` |
|
||||
| 测试内部 assert | 纯验证数据存在,非业务查询 | `Model._default_manager.get(pk=...)` |
|
||||
|
||||
### 14.4 Lint 规则(pre-commit)
|
||||
|
||||
在 `.pre-commit-config.yaml` 增加以下规则,阻断直接 `objects` 调用:
|
||||
|
||||
```yaml
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: no-raw-objects
|
||||
name: "禁止直接使用 Model.objects(业务代码)"
|
||||
language: pygrep
|
||||
entry: '(?<!_default_manager)\.\bObjects\b|(?<!\._default_manager)\.objects\.(filter|all|get|exclude|create|update|delete)\b'
|
||||
files: ^apps/.*\.py$
|
||||
exclude: (tests/|migrations/|factories/)
|
||||
types: [python]
|
||||
```
|
||||
|
||||
### 14.5 权限边界测试矩阵
|
||||
|
||||
每个受权限保护的 Model,集成测试必须覆盖以下 3 case:
|
||||
|
||||
| Case | 说明 | 预期结果 |
|
||||
|---|---|---|
|
||||
| own | 员工查自己负责的数据 | 返回数据 ✅ |
|
||||
| department | 店长查本部门数据 | 按角色返回 ✅ |
|
||||
| cross_department_denied | 普通员工跨部门查询 | 空集或 403 ✅ |
|
||||
|
||||
|
||||
|
||||
@@ -35,10 +35,11 @@
|
||||
| 03 | P0-A | 房源管理(详情) | US-PROPERTY-003~008 | `UI_DESIGN/房源管理/房源详情_UI.md` | | `UI_DESIGN/房源详情_UI.html` | 待评审 | 你评审房源详情 UI.md + 静态页,给我反馈我再迭代 |
|
||||
| 04 | P0-B | 楼盘管理(列表) | US-COMPLEX-002 | `UI_DESIGN/楼盘管理/楼盘列表_UI.md` | | `UI_DESIGN/楼盘列表_UI.html` | 待评审 | 你评审楼盘列表 UI.md + 静态页,给我反馈我再迭代 |
|
||||
| 05 | P0-B | 楼盘管理(详情/维护) | US-COMPLEX-001 | `UI_DESIGN/楼盘管理/楼盘详情_UI.md` | `Project/fonrey/screenshots/楼盘管理/楼盘管理.png`<br>`Project/fonrey/screenshots/楼盘管理/楼栋管理.png`<br> | `UI_DESIGN/楼盘详情_UI.html` | 待评审 | 进入任务06(楼盘管理-区域管理) |
|
||||
| 06 | P0-B | 楼盘管理(区域) | US-COMPLEX-003 | `UI_DESIGN/楼盘管理/区域管理_UI.md` | `Project/fonrey/screenshots/楼盘管理/区域管理.png` | `UI_DESIGN/区域管理_UI.html` | 待设计 | 完成任务05后开始 |
|
||||
| 07 | P0-C | 组织人事 | US-ORG-001~003 | `UI_DESIGN/组织人事管理/组织人事_UI.md` | `Project/fonrey/screenshots/组织人事/组织结构/公司组织结构.png` | `UI_DESIGN/组织人事_UI.html` | 待设计 | 完成任务06后开始 |
|
||||
| 08 | P0-C | 权限管理 | US-PERMISSION-001~005 | `UI_DESIGN/权限管理/权限管理_UI.md`<br>`Project/fonrey/PRD/权限管理/房源-二手租赁.md`<br>`Project/fonrey/PRD/权限管理/客源.md` | `Project/fonrey/screenshots/权限管理/权限-客源-客源.png`<br>``<br>`Project/fonrey/screenshots/权限管理/权限-房源-二手租赁.jpg`<br>`` | `UI_DESIGN/权限管理_UI.html` | 待设计 | 完成任务07后开始 |
|
||||
| 09 | P0-C | 系统配置 | US-SETTING-001-A/B/C | `UI_DESIGN/系统配置/系统配置_UI.md` | | `UI_DESIGN/系统配置_UI.html` | 待设计 | 完成任务08后开始 |
|
||||
| 06 | P0-B | 楼盘管理(区域) | US-COMPLEX-003 | `UI_DESIGN/楼盘管理/区域管理_UI.md` | `Project/fonrey/screenshots/楼盘管理/区域管理.png` | `UI_DESIGN/区域管理_UI.html` | 待评审 | 你评审区域管理 UI.md + 静态页,给我反馈我再迭代 |
|
||||
| 07 | P0-C | 组织人事 | US-ORG-001~003 | `UI_DESIGN/组织人事管理/组织人事_UI.md` | `Project/fonrey/screenshots/组织人事/组织结构/公司组织结构.png` | `UI_DESIGN/组织人事_UI.html` | 待评审 | 你评审组织人事 UI.md + 静态页,给我反馈我再迭代 |
|
||||
| 08 | P0-C | 权限管理 | US-PERMISSION-001~005 | `UI_DESIGN/权限管理/权限管理_UI.md`<br>`Project/fonrey/PRD/权限管理/房源-二手租赁.md`<br>`Project/fonrey/PRD/权限管理/客源.md` | `Project/fonrey/screenshots/权限管理/权限-客源-客源.png`<br>``<br>`Project/fonrey/screenshots/权限管理/权限-房源-二手租赁.jpg`<br>`` | `UI_DESIGN/权限管理_UI.html` | 待评审 | 你评审权限管理 UI.md + 静态页,给我反馈我再迭代 |
|
||||
| 09 | P0-C | 系统配置 | US-SETTING-001-A/B/C | `UI_DESIGN/系统配置/系统配置_UI.md` | `Project/fonrey/screenshots/设置/客源设置-客源参数配置.png`<br>`Project/fonrey/screenshots/设置/房源设置-字段标签设置.png`<br>`Project/fonrey/screenshots/设置/房源设置-字段标签设置-修改字段必填要求.png`<br>`Project/fonrey/screenshots/设置/房源设置-字段标签设置-自定义预设参数.png`<br>`Project/fonrey/screenshots/设置/客源设置-基本配置.jpg` | `UI_DESIGN/系统配置_UI.html` | 待评审 | 你评审系统配置 UI.md + 静态页,给我反馈我再迭代 |
|
||||
| 10 | P1-C | 系统配置(首页设置) | US-SETTING-010 | `UI_DESIGN/系统配置/首页设置_UI.md` | `Project/fonrey/screenshots/设置/首页设置.png` | `UI_DESIGN/首页设置_UI.html` | 待评审 | 你评审首页设置 UI.md + 静态页,给我反馈我再迭代 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
1119
Project/fonrey/UI_DESIGN/区域管理_UI.html
Normal file
1119
Project/fonrey/UI_DESIGN/区域管理_UI.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -20,10 +20,10 @@
|
||||
400:'#94A3B8',500:'#64748B',600:'#475569',700:'#334155',
|
||||
800:'#1E293B',900:'#0F172A',
|
||||
},
|
||||
success:{50:'#F0FDF4',600:'#16A34A',700:'#15803D'},
|
||||
warning:{50:'#FFFBEB',600:'#D97706',700:'#B45309'},
|
||||
danger: {50:'#FEF2F2',600:'#DC2626',700:'#B91C1C'},
|
||||
info: {50:'#EFF6FF',600:'#2563EB',700:'#1D4ED8'},
|
||||
success:{50:'#F0FDF4',600:'#16A34A'},
|
||||
warning:{50:'#FFFBEB',600:'#D97706'},
|
||||
danger: {50:'#FEF2F2',600:'#DC2626'},
|
||||
info: {50:'#EFF6FF',600:'#2563EB'},
|
||||
},
|
||||
boxShadow: { xs:'0 1px 2px rgba(15,23,42,0.04)' },
|
||||
fontFamily: {
|
||||
@@ -174,7 +174,7 @@
|
||||
complex: '中远两湾城',
|
||||
block_no: '12', unit_no: '2', room_no: '802',
|
||||
district: '普陀', business_area: '长寿路',
|
||||
status: 'for_sale', status_label: '出售', status_cls: 'bg-success-50 text-success-700',
|
||||
status: 'for_sale', status_label: '出售', status_cls: 'bg-success-50 text-success-600',
|
||||
transaction_type: 'sale', transaction_label: '买卖', transaction_cls: 'bg-primary-200 text-primary-800',
|
||||
tags: [
|
||||
{ label: '满五', cls: 'bg-success-50 text-success-600' },
|
||||
@@ -192,7 +192,7 @@
|
||||
complex: '张江花园',
|
||||
block_no: '3', unit_no: '1', room_no: '1501',
|
||||
district: '浦东', business_area: '张江',
|
||||
status: 'for_sale', status_label: '出售', status_cls: 'bg-success-50 text-success-700',
|
||||
status: 'for_sale', status_label: '出售', status_cls: 'bg-success-50 text-success-600',
|
||||
transaction_type: 'sale', transaction_label: '买卖', transaction_cls: 'bg-primary-200 text-primary-800',
|
||||
tags: [
|
||||
{ label: '满五', cls: 'bg-success-50 text-success-600' },
|
||||
@@ -212,7 +212,7 @@
|
||||
status: 'for_sale_rent', status_label: '租售', status_cls: 'bg-primary-50 text-primary-700',
|
||||
transaction_type: 'sale_rent', transaction_label: '租售', transaction_cls: 'bg-neutral-300 text-neutral-800',
|
||||
tags: [
|
||||
{ label: '私', cls: 'bg-warning-50 text-warning-700' },
|
||||
{ label: '私', cls: 'bg-warning-50 text-warning-600' },
|
||||
{ label: '速销', cls: 'bg-danger-50 text-danger-600' }
|
||||
],
|
||||
sale_price: '850', sale_unit_price: '70,833',
|
||||
@@ -256,7 +256,7 @@
|
||||
complex: '泰晤士小镇',
|
||||
block_no: '8', unit_no: '4', room_no: '1102',
|
||||
district: '松江', business_area: '泰晤士小镇',
|
||||
status: 'for_sale', status_label: '出售', status_cls: 'bg-success-50 text-success-700',
|
||||
status: 'for_sale', status_label: '出售', status_cls: 'bg-success-50 text-success-600',
|
||||
transaction_type: 'sale', transaction_label: '买卖', transaction_cls: 'bg-primary-200 text-primary-800',
|
||||
tags: [
|
||||
{ label: '满五', cls: 'bg-success-50 text-success-600' },
|
||||
@@ -274,10 +274,10 @@
|
||||
complex: '陆家嘴滨江凯旋门',
|
||||
block_no: '2', unit_no: '1', room_no: '3801',
|
||||
district: '浦东', business_area: '陆家嘴',
|
||||
status: 'for_sale', status_label: '出售', status_cls: 'bg-success-50 text-success-700',
|
||||
status: 'for_sale', status_label: '出售', status_cls: 'bg-success-50 text-success-600',
|
||||
transaction_type: 'sale', transaction_label: '买卖', transaction_cls: 'bg-primary-200 text-primary-800',
|
||||
tags: [
|
||||
{ label: '私', cls: 'bg-warning-50 text-warning-700' },
|
||||
{ label: '私', cls: 'bg-warning-50 text-warning-600' },
|
||||
{ label: '速销', cls: 'bg-danger-50 text-danger-600' },
|
||||
{ label: '视频', cls: 'bg-neutral-100 text-neutral-600' }
|
||||
],
|
||||
@@ -420,8 +420,8 @@
|
||||
<div x-data="{ show: true }" x-show="show"
|
||||
class="mt-2 flex items-center gap-2 px-3 py-2 bg-warning-50 border border-warning-200 rounded-lg text-sm">
|
||||
<svg class="w-4 h-4 text-warning-600 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"/></svg>
|
||||
<span class="text-warning-700">
|
||||
<a href="#" class="font-medium text-warning-700 hover:underline">配置关注小区</a>
|
||||
<span class="text-warning-600">
|
||||
<a href="#" class="font-medium text-warning-600 hover:underline">配置关注小区</a>
|
||||
(关注小区后,当该小区产生对应交易类型下的新上房源、降价房源时,系统将第一时间通知您,提升您的作业效率哦!)
|
||||
</span>
|
||||
<button @click="show = false" class="ml-auto text-neutral-400 hover:text-neutral-600 shrink-0">
|
||||
|
||||
@@ -24,20 +24,20 @@
|
||||
<style>
|
||||
:root {
|
||||
--bg-page:#F8FAFC; --bg-panel:#FFF; --bg-subtle:#F1F5F9; --text-main:#1E293B; --text-sub:#64748B;
|
||||
--border:#E2E8F0; --input-bg:#FFF; --input-text:#1E293B; --header-bg:#FFFFFFD9;
|
||||
--border:#E2E8F0; --input-bg:#FFF; --input-text:#1E293B; --header-bg:#FFF;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--bg-page:#0B1220; --bg-panel:#111A2E; --bg-subtle:#1A253C; --text-main:#E2E8F0; --text-sub:#94A3B8;
|
||||
--border:#334155; --input-bg:#0F172A; --input-text:#E2E8F0; --header-bg:#0F172AD9;
|
||||
--bg-page:#0F172A; --bg-panel:#1E293B; --bg-subtle:#334155; --text-main:#E2E8F0; --text-sub:#94A3B8;
|
||||
--border:#334155; --input-bg:#0F172A; --input-text:#E2E8F0; --header-bg:#0F172A;
|
||||
}
|
||||
[data-theme="system"] {
|
||||
--bg-page:#F8FAFC; --bg-panel:#FFF; --bg-subtle:#F1F5F9; --text-main:#1E293B; --text-sub:#64748B;
|
||||
--border:#E2E8F0; --input-bg:#FFF; --input-text:#1E293B; --header-bg:#FFFFFFD9;
|
||||
--border:#E2E8F0; --input-bg:#FFF; --input-text:#1E293B; --header-bg:#FFF;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
[data-theme="system"] {
|
||||
--bg-page:#0B1220; --bg-panel:#111A2E; --bg-subtle:#1A253C; --text-main:#E2E8F0; --text-sub:#94A3B8;
|
||||
--border:#334155; --input-bg:#0F172A; --input-text:#E2E8F0; --header-bg:#0F172AD9;
|
||||
--bg-page:#0F172A; --bg-panel:#1E293B; --bg-subtle:#334155; --text-main:#E2E8F0; --text-sub:#94A3B8;
|
||||
--border:#334155; --input-bg:#0F172A; --input-text:#E2E8F0; --header-bg:#0F172A;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
223
Project/fonrey/UI_DESIGN/权限管理/权限管理_UI.md
Normal file
223
Project/fonrey/UI_DESIGN/权限管理/权限管理_UI.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# 权限管理 UI 设计文档
|
||||
|
||||
> **任务编号**:08(P0-C)
|
||||
> **覆盖范围**:`US-PERMISSION-001 ~ US-PERMISSION-005`
|
||||
> **输出文件**:`UI_DESIGN/权限管理_UI.html`
|
||||
> **设计基线**:`UI_SYSTEM/UI_SYSTEM.md`(后台壳层、筛选区、表格、分页、Modal/Drawer)
|
||||
> **需求依据**:
|
||||
> - `PRD/TASK.md`(US-PERMISSION-001~005)
|
||||
> - `PRD/权限管理/权限管理模块PRD.md`
|
||||
> - `DATA_MODEL/DATA_MODEL_PERMISSION.md`
|
||||
> - `PRD/权限管理/首页.md`
|
||||
> - `PRD/权限管理/客源.md`
|
||||
> - `PRD/权限管理/房源-二手租赁.md`
|
||||
> **竞品截图参考**:
|
||||
> - `screenshots/权限管理/权限管理-人员列表.png`
|
||||
> - `screenshots/权限管理/权限管理-批量设置角色.png`
|
||||
> - `screenshots/权限管理/权限管理-人员编辑特定权限.png`
|
||||
> - `screenshots/权限管理/权限管理-修改个人权限-客源.png`
|
||||
> - `screenshots/权限管理/角色管理-角色列表.png`
|
||||
> - `screenshots/权限管理/角色管理-添加角色1.png`
|
||||
> - `screenshots/权限管理/角色管理-修改角色.png`
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
### 1.1 页面目标
|
||||
|
||||
本页用于完成权限管理模块 MVP 主链路:
|
||||
|
||||
1. **角色配置(US-PERMISSION-001)**
|
||||
- 角色列表(预设 + 自定义)
|
||||
- 新增自定义角色
|
||||
- 预设角色不可删
|
||||
- 可进入角色权限配置抽屉
|
||||
2. **人员权限列表管理(US-PERMISSION-002)**
|
||||
- 按姓名/工号、部门、角色、职务检索
|
||||
- 列表展示角色、管理范围、不一致标记
|
||||
3. **批量设置角色(US-PERMISSION-003)**
|
||||
- 勾选员工后批量赋角色
|
||||
- 覆盖个人权限提醒
|
||||
4. **功能权限控制(US-PERMISSION-004)**
|
||||
- 人员权限编辑页支持模块级开关 + 权限项配置
|
||||
- 模块关闭时视为菜单隐藏
|
||||
5. **数据权限控制(US-PERMISSION-005)**
|
||||
- 范围型权限(本人/本组/本门店/本区域/本大区/全公司)
|
||||
- 管理范围详情弹窗
|
||||
|
||||
### 1.2 任务边界
|
||||
|
||||
- ✅ 包含:权限管理 Tab、角色管理 Tab、批量角色弹窗、个人权限抽屉、权限项详情抽屉、角色新增弹窗、角色配置抽屉、范围详情弹窗、分页/空态/Toast
|
||||
- ✅ 包含:必填校验、角色唯一性校验、预设角色删除限制、批量按钮禁用态
|
||||
- ⛔ 不包含:真实权限后端、真实 Redis 失效、真实 403 路由拦截、真实日志系统
|
||||
|
||||
---
|
||||
|
||||
## 2. 信息架构
|
||||
|
||||
### 2.1 页面骨架
|
||||
|
||||
- **Top Bar**:系统一级导航(人事主入口高亮)
|
||||
- **Sidebar**:组织人事二级菜单(权限管理高亮)
|
||||
- **Main Content**:
|
||||
1. 面包屑:`人事OA / 组织人事 / 权限管理`
|
||||
2. 主 Tab:`权限管理`(人员维度)/ `角色管理`(角色维度)
|
||||
|
||||
### 2.2 权限管理 Tab(人员维度)
|
||||
|
||||
1. 筛选区
|
||||
- 姓名/工号
|
||||
- 员工部门
|
||||
- 角色
|
||||
- 职务名称
|
||||
- 权限与角色权限不一致(快捷筛选)
|
||||
2. 操作区
|
||||
- 批量设置角色
|
||||
- 导出
|
||||
3. 人员表格
|
||||
- 复选框、员工姓名、工号、部门、职务、角色、管理范围(详情)、操作
|
||||
4. 行操作
|
||||
- 修改权限
|
||||
- 复制角色
|
||||
- 扩充范围
|
||||
- 范围
|
||||
5. 分页区
|
||||
|
||||
### 2.3 角色管理 Tab(角色维度)
|
||||
|
||||
1. 筛选区
|
||||
- 角色名称
|
||||
- 角色类别
|
||||
- 修改时间范围
|
||||
2. 操作区
|
||||
- 新增角色
|
||||
- 批量删除角色
|
||||
3. 角色表格
|
||||
- 复选框、角色名称、角色类别、应用人数(查看)、引用角色、创建时间、修改时间、操作
|
||||
4. 行操作
|
||||
- 编辑(进入角色权限配置)
|
||||
- 删除(预设角色禁删)
|
||||
- 修改日志
|
||||
|
||||
### 2.4 关键弹层
|
||||
|
||||
- 批量设置角色 Modal
|
||||
- 新增角色 Modal
|
||||
- 人员权限编辑 Drawer
|
||||
- 权限项详情 Drawer(显示角色应用人数及名单)
|
||||
- 管理范围详情 Modal
|
||||
- 角色权限配置 Drawer
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键交互与校验
|
||||
|
||||
### 3.1 批量设置角色(US-PERMISSION-003)
|
||||
|
||||
- 未勾选员工时按钮禁用
|
||||
- 打开弹窗后 `角色` 为必填
|
||||
- 若选中员工存在个人覆盖权限,显示提示:
|
||||
- `该操作将覆盖所选员工的个人自定义权限,请确认`
|
||||
- 提交成功:
|
||||
- 批量更新角色
|
||||
- 清除个人不一致标记
|
||||
- Toast 成功提示
|
||||
|
||||
### 3.2 新增角色(US-PERMISSION-001)
|
||||
|
||||
- 必填字段:
|
||||
- 角色名称
|
||||
- 角色类别
|
||||
- 校验:
|
||||
- 角色名称同租户唯一
|
||||
- 成功后:
|
||||
- 角色入列表
|
||||
- 默认可进入权限配置抽屉
|
||||
|
||||
### 3.3 删除角色(US-PERMISSION-001)
|
||||
|
||||
- 预设角色 (`isBuiltin=true`) 禁止删除
|
||||
- 应用人数 > 0 时禁止删除
|
||||
- 删除成功后刷新列表并 Toast
|
||||
|
||||
### 3.4 人员权限编辑(US-PERMISSION-004)
|
||||
|
||||
- 左侧模块树切换,右侧权限项按分组展示
|
||||
- 模块总开关关闭后,该模块权限项置灰(模拟菜单隐藏)
|
||||
- 权限项支持三类控件:
|
||||
- 开关型(BOOLEAN)
|
||||
- 范围型(SCOPE)
|
||||
- 数值型(INTEGER)
|
||||
- 点击 `编辑` 打开单项详情抽屉
|
||||
- 保存后:
|
||||
- 标记该员工为“权限与角色不一致”
|
||||
- 人员列表可被快捷筛出
|
||||
|
||||
### 3.5 数据范围与管理范围(US-PERMISSION-005)
|
||||
|
||||
- 人员列表 `管理范围` 列展示范围摘要
|
||||
- 点击 `详情` 打开范围详情弹窗
|
||||
- 范围项支持:`无 / 本人 / 本组 / 本门店 / 本区域 / 本大区 / 全公司`
|
||||
|
||||
---
|
||||
|
||||
## 4. 状态矩阵
|
||||
|
||||
| 状态 | 触发 | 页面反馈 |
|
||||
|---|---|---|
|
||||
| 默认态 | 页面初次进入 | 权限管理 Tab 默认选中 |
|
||||
| 查询态 | 输入筛选后点击查询 | 列表按条件过滤 |
|
||||
| 空结果 | 条件无匹配 | 表格空态文案“暂无匹配人员/角色” |
|
||||
| 勾选态 | 勾选员工/角色 | 批量按钮启用 |
|
||||
| 批量角色校验失败 | 角色未选 | 字段下错误文案 |
|
||||
| 新增角色校验失败 | 名称/类别为空或重名 | 字段错误文案 |
|
||||
| 删除受限 | 内置角色/有应用人数 | Toast 错误提示 |
|
||||
| 权限保存成功 | 个人或角色权限保存 | 列表状态更新 + Toast |
|
||||
| 主题策略 | 后台统一浅色体系 | 页面内不包含 Light/Dark/System 控件 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 字段与数据模型映射(DATA_MODEL_PERMISSION)
|
||||
|
||||
| UI 字段 | 数据模型字段 |
|
||||
|---|---|
|
||||
| 角色名称 | `roles.name` |
|
||||
| 角色类别 | `roles.category` |
|
||||
| 角色来源模板 | `roles.template_role_id` |
|
||||
| 是否内置角色 | `roles.is_system_builtin` |
|
||||
| 员工角色关联 | `staff_roles.staff_id / role_id / is_primary` |
|
||||
| 个人权限覆盖 | `staff_permission_overrides.*` |
|
||||
| 权限定义 | `permission_defs.code / value_type / default_value` |
|
||||
| 角色权限值 | `role_permissions.value` |
|
||||
| 员工管理范围 | `staff_data_scopes.scope_type / org_unit_id` |
|
||||
| 权限变更日志入口 | `permission_change_logs`(本页仅展示入口) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 可访问性与实现规范
|
||||
|
||||
- 表格头部使用 `<th scope="col">`
|
||||
- 弹窗可通过 `Esc` 关闭
|
||||
- 必填错误使用明确文本,不仅依赖颜色
|
||||
- 按钮禁用态提供 `disabled` + 视觉弱化
|
||||
- 行内链接均保留可点击区域(>= 28px 高)
|
||||
- 保持后台浅色设计,不加入主题切换组件
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收清单(任务08)
|
||||
|
||||
- [x] 覆盖 US-PERMISSION-001:角色列表 + 新增角色 + 预设角色不可删
|
||||
- [x] 覆盖 US-PERMISSION-002:人员权限列表筛选、分页、操作入口
|
||||
- [x] 覆盖 US-PERMISSION-003:批量设置角色弹窗 + 覆盖提醒
|
||||
- [x] 覆盖 US-PERMISSION-004:模块级开关 + 权限项编辑 + 保存
|
||||
- [x] 覆盖 US-PERMISSION-005:范围型权限 + 管理范围详情
|
||||
- [ ] 控制台 0 报错(待本地预览验证)
|
||||
|
||||
---
|
||||
|
||||
## 8. 后续衔接
|
||||
|
||||
- 任务08评审后进入任务09(系统配置)
|
||||
- 权限管理结果将与组织人事(员工/部门)及后续业务模块菜单展示联动
|
||||
1193
Project/fonrey/UI_DESIGN/权限管理_UI.html
Normal file
1193
Project/fonrey/UI_DESIGN/权限管理_UI.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -87,8 +87,8 @@
|
||||
line-height: 1;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #FECACA;
|
||||
color: #B91C1C;
|
||||
border: 1px solid #DC2626;
|
||||
color: #DC2626;
|
||||
background: #FEF2F2;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
[x-cloak] { display: none !important; }
|
||||
.tabular-nums { font-variant-numeric: tabular-nums; }
|
||||
.captcha-track { background: linear-gradient(90deg, #E2E8F0 0%, #F1F5F9 100%); }
|
||||
.captcha-success { background: linear-gradient(90deg, #dcfce7 0%, #bbf7d0 100%); }
|
||||
.captcha-success { background: linear-gradient(90deg, #F0FDF4 0%, #16A34A 100%); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen bg-neutral-50 font-sans text-sm text-neutral-700 antialiased" x-data="loginPrototype()">
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<style>
|
||||
[x-cloak] { display: none !important; }
|
||||
.captcha-track { background: linear-gradient(90deg, #E2E8F0 0%, #F1F5F9 100%); }
|
||||
.captcha-success { background: linear-gradient(90deg, #dcfce7 0%, #bbf7d0 100%); }
|
||||
.captcha-success { background: linear-gradient(90deg, #F0FDF4 0%, #16A34A 100%); }
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-4px); }
|
||||
|
||||
179
Project/fonrey/UI_DESIGN/系统配置/系统配置_UI.md
Normal file
179
Project/fonrey/UI_DESIGN/系统配置/系统配置_UI.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# 系统配置 UI 设计文档
|
||||
|
||||
> **任务编号**:09(P0-C)
|
||||
> **覆盖范围**:`US-SETTING-001-A / US-SETTING-001-B / US-SETTING-001-C`
|
||||
> **输出文件**:`UI_DESIGN/系统配置_UI.html`
|
||||
> **设计基线**:`UI_SYSTEM/UI_SYSTEM.md`(后台壳层、筛选区、表格、Modal/Drawer、表单校验)
|
||||
> **需求依据**:
|
||||
> - `PRD/TASK.md`(系统配置章节,US-SETTING-001-A/B/C)
|
||||
> - `PRD/系统配置/系统配置模块PRD.md`
|
||||
> - `PRD/系统配置/系统配置参数数据.md`
|
||||
> - `DATA_MODEL/DATA_MODEL_SETTING.md`
|
||||
> - `DATA_MODEL/ENUMS.md`
|
||||
> **竞品截图参考**:
|
||||
> - `screenshots/设置/客源设置-客源参数配置.png`
|
||||
> - `screenshots/设置/客源设置-基本配置.jpg`
|
||||
> - `screenshots/设置/房源设置-字段标签设置.png`
|
||||
> - `screenshots/设置/房源设置-字段标签设置-修改字段必填要求.png`
|
||||
> - `screenshots/设置/房源设置-字段标签设置-自定义预设参数.png`
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
### 1.1 页面目标
|
||||
|
||||
本页用于承载系统配置模块 MVP 的三条核心能力:
|
||||
|
||||
1. **参数配置(US-SETTING-001-A)**
|
||||
- 管理租户可配置枚举(`lookup_items`):客源来源、跟进目的、房源来源
|
||||
- 支持新增项目值、调整排序、停用/启用
|
||||
- 系统预制项(`is_system=true`)不可删除
|
||||
2. **房源字段规则(US-SETTING-001-B)**
|
||||
- 以「用途 × 交易状态」查看规则矩阵
|
||||
- 支持字段三态:必填 / 选填 / 隐藏(`required/optional/hidden`)
|
||||
- MVP 字段范围:朝向、装修、楼层、建筑面积、套内面积、房型、产权年限、车位数
|
||||
3. **客源规则(US-SETTING-001-C)**
|
||||
- 配置新增私客查重范围:本人 / 本部门 / 全公司
|
||||
- 配置客源必填字段(等级、来源默认必填)
|
||||
- 保存后提示规则生效与缓存失效
|
||||
|
||||
### 1.2 任务边界
|
||||
|
||||
- ✅ 包含:系统设置壳层、三主 Tab、配置表格、参数编辑弹窗、字段规则编辑抽屉、规则保存反馈
|
||||
- ✅ 包含:新增项必填校验、重复值校验、系统项删除限制、客源必填字段保护校验
|
||||
- ⛔ 不包含:真实后端 API、真实 Redis 调用、真实跨页面即时联动
|
||||
|
||||
---
|
||||
|
||||
## 2. 信息架构
|
||||
|
||||
### 2.1 页面骨架
|
||||
|
||||
- **Top Bar**:一级导航(系统模块高亮)
|
||||
- **Sidebar**:设置目录(系统配置高亮)
|
||||
- **Main Content**:
|
||||
1. 面包屑:`系统 / 设置 / 系统配置`
|
||||
2. 主 Tab:`参数配置` / `房源字段规则` / `客源规则`
|
||||
|
||||
### 2.2 参数配置(US-SETTING-001-A)
|
||||
|
||||
1. 筛选区
|
||||
- 关键字(参数项/项目值)
|
||||
- 模块筛选(客源/房源)
|
||||
2. 参数分组卡片(3组)
|
||||
- 客源来源(`client.source`)
|
||||
- 跟进目的(`client.follow_purpose`)
|
||||
- 房源来源(`property.source`)
|
||||
3. 分组表格列
|
||||
- 项目值、排序、状态、来源(预制/自定义)、操作
|
||||
4. 编辑弹窗(分组级)
|
||||
- 项目值输入(可多行)
|
||||
- 上移/下移排序
|
||||
- 新增项目
|
||||
- 删除(系统预制项禁用)
|
||||
- 启用/停用切换
|
||||
|
||||
### 2.3 房源字段规则(US-SETTING-001-B)
|
||||
|
||||
1. 规则矩阵列表(组合级)
|
||||
- 住宅×出售、住宅×出租、商铺×出售、商铺×出租
|
||||
2. 组合列表列
|
||||
- 用途、交易状态、必填字段数、隐藏字段数、更新时间、操作
|
||||
3. 规则编辑抽屉
|
||||
- 字段名称
|
||||
- 必填/选填/隐藏(Radio)
|
||||
- 录入页展示(Switch,关闭即 hidden)
|
||||
|
||||
### 2.4 客源规则(US-SETTING-001-C)
|
||||
|
||||
1. 查重范围卡片
|
||||
- 单选:本人 / 本部门 / 全公司
|
||||
- 每个选项附说明文案
|
||||
2. 必填字段卡片
|
||||
- 等级、来源(锁定为必填)
|
||||
- 总价区间、居室需求、购房目的(可配置)
|
||||
3. 保存反馈卡片
|
||||
- 说明保存后缓存 key `{tenant_schema}:setting:client_rules` 失效
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心交互与校验
|
||||
|
||||
### 3.1 参数配置弹窗
|
||||
|
||||
- 新增空项目后点击保存:提示 `项目值不能为空`
|
||||
- 同一分组内重名:提示 `项目值不可重复`
|
||||
- 系统预制项删除:禁止,提示 `系统预制项不可删除,仅可停用`
|
||||
- 保存成功:
|
||||
- 回写分组列表
|
||||
- Toast:`参数已保存,缓存失效:{tenant_schema}:setting:lookup:{module}.{key}`
|
||||
|
||||
### 3.2 字段规则编辑抽屉
|
||||
|
||||
- 每个字段仅允许三态之一(required/optional/hidden)
|
||||
- 关闭“录入页展示”开关时自动设为 `hidden`
|
||||
- 开启“录入页展示”且当前为 hidden 时自动回退为 `optional`
|
||||
- 保存成功:
|
||||
- 更新矩阵统计(必填数/隐藏数)
|
||||
- Toast:`字段规则已保存,缓存失效:{tenant_schema}:setting:field_req:{module}.{entity_type}.{trade_status}`
|
||||
|
||||
### 3.3 客源规则保存
|
||||
|
||||
- `等级`、`来源`不得取消必填
|
||||
- 若取消则拦截并提示:`等级、来源为默认必填字段,不可取消`
|
||||
- 保存成功提示:
|
||||
- `客源规则已保存,缓存失效:{tenant_schema}:setting:client_rules`
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据模型与配置映射
|
||||
|
||||
### 4.1 Lookup 映射(001-A)
|
||||
|
||||
- `lookup_groups.module + key`:参数分组
|
||||
- `lookup_items`:项目值列表
|
||||
- UI 字段映射:
|
||||
- 项目值 → `label_zh`
|
||||
- 排序 → `sort_order`
|
||||
- 状态 → `is_active`
|
||||
- 来源 → `is_system`
|
||||
|
||||
### 4.2 字段规则映射(001-B)
|
||||
|
||||
- 表:`field_requirement_rules`
|
||||
- 维度:`module + entity_type + trade_status + field_key`
|
||||
- 规则值:`requirement`(required/optional/hidden)
|
||||
|
||||
### 4.3 客源规则映射(001-C)
|
||||
|
||||
- 表:`tenant_settings`
|
||||
- `category=client, key=duplicate_check_scope`
|
||||
- 客源必填字段规则以规则快照形式保存(原型中以本地对象模拟)
|
||||
|
||||
---
|
||||
|
||||
## 5. 状态矩阵
|
||||
|
||||
| 区域 | 默认态 | 编辑态 | 保存成功 | 校验失败 |
|
||||
|---|---|---|---|---|
|
||||
| 参数配置 | 分组列表展示 | 弹窗内增删改排序 | 列表更新 + Toast | 错误文案红字 |
|
||||
| 字段规则 | 矩阵列表展示 | 抽屉内逐字段三态设置 | 统计更新 + Toast | 保持抽屉不关闭 |
|
||||
| 客源规则 | 查重范围 + 必填项配置 | 单选/勾选编辑 | Toast + 状态持久 | 顶部错误提示 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 验收清单(静态页)
|
||||
|
||||
1. 三主 Tab 切换后内容区替换且壳层不变
|
||||
2. 参数配置弹窗覆盖:空值校验、重复值校验、系统项删除限制
|
||||
3. 房源字段规则抽屉覆盖:三态切换、显示开关联动、保存回写统计
|
||||
4. 客源规则覆盖:查重范围切换、默认必填保护、保存提示
|
||||
5. 控制台 0 报错后方可进入待评审
|
||||
|
||||
---
|
||||
|
||||
## 7. 交付物
|
||||
|
||||
- `UI_DESIGN/系统配置/系统配置_UI.md`
|
||||
- `UI_DESIGN/系统配置_UI.html`
|
||||
186
Project/fonrey/UI_DESIGN/系统配置/首页设置_UI.md
Normal file
186
Project/fonrey/UI_DESIGN/系统配置/首页设置_UI.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# 首页设置 UI 设计文档
|
||||
|
||||
> **任务编号**:10(P1-C)
|
||||
> **覆盖范围**:`US-SETTING-010`(管理员配置首页展示内容)
|
||||
> **输出文件**:`UI_DESIGN/首页设置_UI.html`
|
||||
> **设计基线**:`UI_SYSTEM/UI_SYSTEM.md`(后台壳层、卡片分组、开关控件、表单校验、保存反馈)
|
||||
> **需求依据**:
|
||||
> - `PRD/TASK.md`(US-SETTING-010)
|
||||
> - `PRD/系统配置/系统配置参数数据.md`(1. 首页设置)
|
||||
> - `PRD/系统配置/系统配置模块PRD.md`(配置模块交互规范)
|
||||
> - `DATA_MODEL/DATA_MODEL_PUBLIC.md`(任务引用)
|
||||
> **竞品截图参考**:
|
||||
> - `screenshots/设置/首页设置.png`
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
### 1.1 页面目标
|
||||
|
||||
本页用于提供“系统配置 → 首页设置”能力,覆盖三个验收点:
|
||||
|
||||
1. **可配置首页展示统计卡片**
|
||||
- 支持勾选/取消卡片(如:今日新增房源、今日新增客源、今日新增带看等)
|
||||
- 支持卡片顺序调整(上移/下移)
|
||||
2. **配置变更后首页实时生效(原型态)**
|
||||
- 编辑区变更实时映射到右侧“首页预览”
|
||||
- 点击保存后固化为当前角色配置
|
||||
3. **不同角色可配置不同首页视图**
|
||||
- 提供角色切换(经纪人 / 店长 / 管理员)
|
||||
- 每个角色独立保存首页模块与卡片配置
|
||||
|
||||
### 1.2 任务边界
|
||||
|
||||
- ✅ 包含:页面壳层、配置分组、开关/单选/输入、角色维度、卡片排序、保存校验、实时预览
|
||||
- ✅ 包含:竞品截图中可见的首页设置分区(员工信息、行程指标、首页业绩、排行榜、成交战报)
|
||||
- ⛔ 不包含:真实后端 API、真实权限控制、真实首页页面联动
|
||||
|
||||
---
|
||||
|
||||
## 2. 信息架构
|
||||
|
||||
### 2.1 页面骨架
|
||||
|
||||
- **Top Bar**:系统一级导航
|
||||
- **Sidebar**:设置菜单(首页设置高亮)
|
||||
- **Main Content**:
|
||||
1. 面包屑:`系统 / 设置 / 首页设置`
|
||||
2. 页面标题:`系统配置-首页设置`
|
||||
3. 搜索框:`请输入设置项名称`
|
||||
4. 角色视图切换(经纪人/店长/管理员)
|
||||
|
||||
### 2.2 配置分区(按竞品还原)
|
||||
|
||||
1. **基本设置**
|
||||
- 编辑态切换(编辑 / 取消 / 保存)
|
||||
2. **员工信息模块**
|
||||
- 是否展示员工司龄(Switch)
|
||||
3. **行程模块显示指标**
|
||||
- 指标范围(买卖 / 租赁)
|
||||
4. **首页业绩显示设置**
|
||||
- 业绩统计口径(单选)
|
||||
5. **首页统计卡片配置(补齐任务验收)**
|
||||
- 卡片启用开关
|
||||
- 顺序上移/下移
|
||||
6. **排行榜设置**
|
||||
- 是否显示业绩和单数
|
||||
- 业绩计算方式
|
||||
- 默认按店或组排名
|
||||
- 默认展示全公司前10排名数据
|
||||
- 过滤账号/过滤部门
|
||||
7. **成交战报设置**
|
||||
- 成交时间范围、成交业绩范围
|
||||
- 是否显示业绩/房源/房源总价
|
||||
|
||||
### 2.3 首页预览区
|
||||
|
||||
- 实时展示当前角色的:
|
||||
- 已启用统计卡片(按配置顺序)
|
||||
- 模块开关结果(员工司龄、排行榜显示项、战报显示项)
|
||||
- 用于验证“配置变更后实时生效(原型级)”
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心交互与校验
|
||||
|
||||
### 3.1 编辑流转
|
||||
|
||||
- 默认只读态:显示“编辑”按钮
|
||||
- 点击“编辑”进入编辑态:
|
||||
- 启用所有表单控件
|
||||
- 出现“取消 / 保存配置”按钮
|
||||
- 点击“取消”恢复到编辑前状态
|
||||
|
||||
### 3.2 保存校验
|
||||
|
||||
1. 至少启用 1 个首页统计卡片
|
||||
- 失败文案:`至少保留 1 个首页统计卡片`
|
||||
2. 过滤账号输入长度 ≤ 100
|
||||
- 失败文案:`过滤账号输入过长,请控制在100字符以内`
|
||||
3. 过滤部门输入长度 ≤ 100
|
||||
- 失败文案:`过滤部门输入过长,请控制在100字符以内`
|
||||
|
||||
保存成功后:
|
||||
- 写入当前角色配置
|
||||
- 退出编辑态
|
||||
- Toast:`首页设置已保存,当前角色视图即时生效`
|
||||
|
||||
### 3.3 角色维度约束
|
||||
|
||||
- 角色切换仅允许在“非编辑态”执行
|
||||
- 编辑态切换角色时提示:`请先保存或取消当前角色编辑内容`
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据映射(原型)
|
||||
|
||||
> 注:原始 TASK 引用 `DATA_MODEL_PUBLIC.md`,未提供首页设置专用字段明细。原型用前端配置对象模拟持久层。
|
||||
|
||||
### 4.1 角色配置结构(模拟)
|
||||
|
||||
```json
|
||||
{
|
||||
"role": "manager",
|
||||
"show_seniority": true,
|
||||
"itinerary_metrics": ["sale", "rent"],
|
||||
"kpi_mode": "pending_and_approved",
|
||||
"ranking": {
|
||||
"show_performance_and_deal": true,
|
||||
"calc_mode": "turn_order",
|
||||
"default_rank_level": "store",
|
||||
"show_company_top10": false,
|
||||
"filter_accounts": "",
|
||||
"filter_departments": ""
|
||||
},
|
||||
"battle_report": {
|
||||
"days_range": "",
|
||||
"amount_range": "",
|
||||
"show_performance": false,
|
||||
"show_property_name": true,
|
||||
"show_property_total_price": false
|
||||
},
|
||||
"home_cards": [
|
||||
{"key": "new_property_today", "enabled": true, "sort": 1},
|
||||
{"key": "new_client_today", "enabled": true, "sort": 2}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 统计卡片枚举(原型)
|
||||
|
||||
- 今日新增房源(`new_property_today`)
|
||||
- 今日新增客源(`new_client_today`)
|
||||
- 今日新增带看(`new_showing_today`)
|
||||
- 今日新增跟进(`new_followup_today`)
|
||||
- 今日签约单量(`signed_deals_today`)
|
||||
- 今日成交业绩(`deal_amount_today`)
|
||||
|
||||
---
|
||||
|
||||
## 5. 状态矩阵
|
||||
|
||||
| 区域 | 默认态 | 编辑态 | 保存成功 | 保存失败 |
|
||||
|---|---|---|---|---|
|
||||
| 基本设置 | 只读展示 | 表单可编辑 | 退出编辑 + Toast | 保持编辑态 + 顶部错误文案 |
|
||||
| 统计卡片 | 显示已启用卡片 | 可启停+排序 | 预览与配置一致 | 至少1项启用校验阻断 |
|
||||
| 角色切换 | 允许切换 | 禁止切换 | 切换后加载角色配置 | 提示先保存/取消 |
|
||||
| 首页预览 | 展示当前角色视图 | 实时反映草稿值 | 与已保存配置一致 | 无 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 验收清单(静态页)
|
||||
|
||||
1. 壳层结构完整(Top Bar + Sidebar + Main)
|
||||
2. 页面分区覆盖截图中的首页设置关键模块
|
||||
3. 角色切换可见不同配置结果
|
||||
4. 统计卡片支持启用/排序,且预览实时变化
|
||||
5. 保存时触发校验与反馈文案
|
||||
6. 本地 `file://` 打开控制台 0 报错
|
||||
|
||||
---
|
||||
|
||||
## 7. 交付物
|
||||
|
||||
- `UI_DESIGN/系统配置/首页设置_UI.md`
|
||||
- `UI_DESIGN/首页设置_UI.html`
|
||||
856
Project/fonrey/UI_DESIGN/系统配置_UI.html
Normal file
856
Project/fonrey/UI_DESIGN/系统配置_UI.html
Normal file
@@ -0,0 +1,856 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1366" />
|
||||
<title>Fonrey 系统配置 · 静态原型</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#F0FDFA',
|
||||
100: '#CCFBF1',
|
||||
200: '#99F6E4',
|
||||
500: '#14B8A6',
|
||||
600: '#0F766E',
|
||||
700: '#115E59',
|
||||
800: '#134E4A'
|
||||
},
|
||||
neutral: {
|
||||
50: '#F8FAFC',
|
||||
100: '#F1F5F9',
|
||||
200: '#E2E8F0',
|
||||
300: '#CBD5E1',
|
||||
400: '#94A3B8',
|
||||
500: '#64748B',
|
||||
600: '#475569',
|
||||
700: '#334155',
|
||||
800: '#1E293B',
|
||||
900: '#0F172A'
|
||||
},
|
||||
success: { 50: '#F0FDF4', 600: '#16A34A' },
|
||||
warning: { 50: '#FFFBEB', 600: '#D97706' },
|
||||
danger: { 50: '#FEF2F2', 600: '#DC2626' },
|
||||
info: { 50: '#EFF6FF', 600: '#2563EB' }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
:root {
|
||||
--bg-page: #F8FAFC;
|
||||
--bg-card: #FFFFFF;
|
||||
--bg-subtle: #F1F5F9;
|
||||
--text-primary: #0F172A;
|
||||
--text-secondary: #64748B;
|
||||
--border: #E2E8F0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[x-cloak] { display: none !important; }
|
||||
|
||||
.main-tab {
|
||||
color: #64748B;
|
||||
border-bottom: 2px solid transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.main-tab.active {
|
||||
color: #115E59;
|
||||
border-bottom-color: #115E59;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-link {
|
||||
font-size: 12px;
|
||||
color: #2563EB;
|
||||
}
|
||||
|
||||
.action-link:hover { text-decoration: underline; }
|
||||
|
||||
.tabular-nums { font-variant-numeric: tabular-nums; }
|
||||
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-thumb { background: #CBD5E1; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #94A3B8; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="text-sm antialiased" x-data="settingPage()" x-init="init()" @keydown.escape.window="closeAllPanels()">
|
||||
<!-- Top Bar -->
|
||||
<header class="fixed top-0 left-0 right-0 h-14 z-30 bg-primary-800 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 px-4 w-60 shrink-0">
|
||||
<div class="w-7 h-7 rounded-md bg-primary-500 flex items-center justify-center text-white text-sm font-semibold">F</div>
|
||||
<span class="text-base font-semibold text-white">Fonrey</span>
|
||||
</div>
|
||||
<nav class="flex items-center gap-1 flex-1 px-2" aria-label="主导航">
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">主页</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">房源</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">客源</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">营销</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">交易</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">数据</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">人事</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md bg-primary-600 text-white font-medium">系统</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">三网</a>
|
||||
</nav>
|
||||
<div class="flex items-center gap-1 px-4 shrink-0">
|
||||
<button class="p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md" aria-label="消息">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022 23.848 23.848 0 0 0 5.454 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 pl-3 ml-1 border-l border-primary-700">
|
||||
<div class="w-8 h-8 rounded-full bg-primary-600 text-white flex items-center justify-center text-sm font-semibold">杜</div>
|
||||
<span class="text-sm font-medium text-primary-100">杜利强</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Side Bar -->
|
||||
<aside class="fixed left-0 top-14 h-[calc(100vh-56px)] w-60 z-20 border-r border-neutral-200 bg-white overflow-y-auto">
|
||||
<nav class="p-3 space-y-0.5">
|
||||
<div class="px-2 pt-2 pb-1 text-xs font-medium text-neutral-500 uppercase tracking-wide">设置</div>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">首页设置</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">房源设置</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">客源设置</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">交易设置</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md bg-primary-50 text-primary-700 font-medium">系统配置</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">人事OA设置</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main -->
|
||||
<main class="ml-60 pt-[72px] min-h-screen px-6 py-5">
|
||||
<div class="mx-auto max-w-[1760px] space-y-4">
|
||||
<section class="bg-white border border-neutral-200 rounded-lg p-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<nav class="flex items-center gap-1 text-xs text-neutral-500 mb-2" aria-label="面包屑">
|
||||
<a href="#" class="hover:text-neutral-700">系统</a>
|
||||
<span>/</span>
|
||||
<a href="#" class="hover:text-neutral-700">设置</a>
|
||||
<span>/</span>
|
||||
<span class="text-neutral-900">系统配置</span>
|
||||
</nav>
|
||||
<h1 class="text-xl font-semibold text-neutral-900">系统配置</h1>
|
||||
<p class="text-xs text-neutral-500 mt-1">覆盖参数配置(Lookup)、房源字段规则、客源录入规则</p>
|
||||
</div>
|
||||
<button class="px-3 py-1.5 rounded-md border border-info-600 text-info-600 bg-white hover:bg-info-50" @click="notify('查看系统配置帮助(原型)')">配置说明</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-white border border-neutral-200 rounded-lg px-4">
|
||||
<nav class="flex items-center gap-6 overflow-x-auto" aria-label="系统配置主Tab">
|
||||
<button class="main-tab py-3" :class="{ 'active': mainTab === 'lookup' }" @click="mainTab = 'lookup'">参数配置</button>
|
||||
<button class="main-tab py-3" :class="{ 'active': mainTab === 'fieldRule' }" @click="mainTab = 'fieldRule'">房源字段规则</button>
|
||||
<button class="main-tab py-3" :class="{ 'active': mainTab === 'clientRule' }" @click="mainTab = 'clientRule'">客源规则</button>
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
<!-- 参数配置 -->
|
||||
<template x-if="mainTab === 'lookup'">
|
||||
<section class="space-y-4">
|
||||
<section class="bg-white border border-neutral-200 rounded-lg p-4 space-y-3">
|
||||
<div class="grid grid-cols-5 gap-3">
|
||||
<input x-model.trim="lookupFilters.keyword" type="text" placeholder="请输入参数项/项目值" class="px-3 py-2 rounded-md border border-neutral-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/30" />
|
||||
<select x-model="lookupFilters.module" class="px-3 py-2 rounded-md border border-neutral-300 bg-white">
|
||||
<option value="">模块(全选)</option>
|
||||
<option value="client">客源</option>
|
||||
<option value="property">房源</option>
|
||||
</select>
|
||||
<div class="col-span-3 flex items-center justify-end gap-2">
|
||||
<button class="px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700">查询</button>
|
||||
<button class="px-3 py-2 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="lookupFilters = { keyword: '', module: '' }">清空条件</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">系统预制项不可删除,仅可停用;保存后触发 lookup 缓存失效。</div>
|
||||
</section>
|
||||
|
||||
<template x-if="filteredLookupGroups.length === 0">
|
||||
<section class="bg-white border border-neutral-200 rounded-lg p-10 text-center text-neutral-400">暂无匹配参数组</section>
|
||||
</template>
|
||||
|
||||
<template x-for="group in filteredLookupGroups" :key="group.id">
|
||||
<section class="bg-white border border-neutral-200 rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-neutral-200 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold" x-text="`${group.label}(${group.moduleLabel})`"></h3>
|
||||
<p class="text-xs text-neutral-500 mt-1" x-text="`Key:${group.module}.${group.key}`"></p>
|
||||
</div>
|
||||
<button class="px-3 py-1.5 rounded-md border border-primary-600 text-primary-600 hover:bg-primary-50" @click="openLookupEditor(group)">编辑参数</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-xs">
|
||||
<thead class="bg-neutral-50 border-b border-neutral-200 text-neutral-500">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">项目值</th>
|
||||
<th class="px-3 py-2 text-left">排序</th>
|
||||
<th class="px-3 py-2 text-left">状态</th>
|
||||
<th class="px-3 py-2 text-left">来源</th>
|
||||
<th class="px-3 py-2 text-left">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-neutral-100 bg-white">
|
||||
<template x-if="group.items.length === 0">
|
||||
<tr><td colspan="5" class="px-3 py-8 text-center text-neutral-400">暂无项目值</td></tr>
|
||||
</template>
|
||||
<template x-for="item in group.items" :key="item.id">
|
||||
<tr>
|
||||
<td class="px-3 py-2" x-text="item.label"></td>
|
||||
<td class="px-3 py-2 tabular-nums" x-text="item.sortOrder"></td>
|
||||
<td class="px-3 py-2">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px]" :class="item.active ? 'bg-success-50 text-success-600' : 'bg-neutral-100 text-neutral-500'" x-text="item.active ? '启用' : '停用'"></span>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px]" :class="item.isSystem ? 'bg-info-50 text-info-600' : 'bg-warning-50 text-warning-600'" x-text="item.isSystem ? '系统预制' : '自定义'"></span>
|
||||
</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">
|
||||
<button class="action-link" @click="toggleLookupItemActive(group.id, item.id)" x-text="item.active ? '停用' : '启用'"></button>
|
||||
<button class="action-link ml-2" :class="item.isSystem ? 'opacity-40 pointer-events-none' : ''" @click="deleteLookupItem(group.id, item.id)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<!-- 房源字段规则 -->
|
||||
<template x-if="mainTab === 'fieldRule'">
|
||||
<section class="space-y-4">
|
||||
<section class="bg-white border border-neutral-200 rounded-lg p-4 space-y-3">
|
||||
<div class="grid grid-cols-5 gap-3">
|
||||
<input x-model.trim="fieldFilters.keyword" type="text" placeholder="请输入用途/交易状态" class="px-3 py-2 rounded-md border border-neutral-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/30" />
|
||||
<select x-model="fieldFilters.entityType" class="px-3 py-2 rounded-md border border-neutral-300 bg-white">
|
||||
<option value="">用途(全选)</option>
|
||||
<option value="residential">住宅</option>
|
||||
<option value="shop">商铺</option>
|
||||
</select>
|
||||
<select x-model="fieldFilters.tradeStatus" class="px-3 py-2 rounded-md border border-neutral-300 bg-white">
|
||||
<option value="">交易状态(全选)</option>
|
||||
<option value="sale">出售</option>
|
||||
<option value="rent">出租</option>
|
||||
</select>
|
||||
<div class="col-span-2 flex items-center justify-end gap-2">
|
||||
<button class="px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700">查询</button>
|
||||
<button class="px-3 py-2 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="fieldFilters = { keyword: '', entityType: '', tradeStatus: '' }">清空条件</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500">规则值映射:required=必填,optional=选填,hidden=隐藏。隐藏字段不在录入页渲染。</p>
|
||||
</section>
|
||||
|
||||
<section class="bg-white border border-neutral-200 rounded-lg overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-xs">
|
||||
<thead class="bg-neutral-50 border-b border-neutral-200 text-neutral-500">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">用途</th>
|
||||
<th class="px-3 py-2 text-left">交易状态</th>
|
||||
<th class="px-3 py-2 text-left">必填字段数</th>
|
||||
<th class="px-3 py-2 text-left">隐藏字段数</th>
|
||||
<th class="px-3 py-2 text-left">更新时间</th>
|
||||
<th class="px-3 py-2 text-left">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-neutral-100 bg-white">
|
||||
<template x-if="filteredFieldCombos.length === 0">
|
||||
<tr><td colspan="6" class="px-3 py-8 text-center text-neutral-400">暂无匹配规则</td></tr>
|
||||
</template>
|
||||
<template x-for="combo in filteredFieldCombos" :key="combo.id">
|
||||
<tr>
|
||||
<td class="px-3 py-2" x-text="combo.entityTypeLabel"></td>
|
||||
<td class="px-3 py-2" x-text="combo.tradeStatusLabel"></td>
|
||||
<td class="px-3 py-2 tabular-nums" x-text="countRequirement(combo, 'required')"></td>
|
||||
<td class="px-3 py-2 tabular-nums" x-text="countRequirement(combo, 'hidden')"></td>
|
||||
<td class="px-3 py-2 tabular-nums" x-text="combo.updatedAt"></td>
|
||||
<td class="px-3 py-2"><button class="action-link" @click="openRuleDrawer(combo)">编辑规则</button></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<!-- 客源规则 -->
|
||||
<template x-if="mainTab === 'clientRule'">
|
||||
<section class="space-y-4">
|
||||
<section class="bg-white border border-neutral-200 rounded-lg p-4 space-y-3">
|
||||
<h3 class="text-base font-semibold">新增私客查重范围</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<template x-for="scope in duplicateScopeOptions" :key="scope.value">
|
||||
<label class="border rounded-md p-3 cursor-pointer" :class="clientRules.duplicateScope === scope.value ? 'border-primary-600 bg-primary-50' : 'border-neutral-200 hover:border-neutral-300'">
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="radio" name="duplicateScope" :value="scope.value" x-model="clientRules.duplicateScope" class="text-primary-600 border-neutral-300" />
|
||||
<span class="font-medium text-sm" x-text="scope.label"></span>
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500 mt-2" x-text="scope.desc"></p>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-white border border-neutral-200 rounded-lg p-4 space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold">客源必填字段配置</h3>
|
||||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="resetClientRules()">恢复默认</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<template x-for="field in clientRequiredFieldOptions" :key="field.key">
|
||||
<label class="flex items-center justify-between border border-neutral-200 rounded-md p-3">
|
||||
<div>
|
||||
<div class="text-sm font-medium" x-text="field.label"></div>
|
||||
<div class="text-xs text-neutral-500" x-text="field.desc"></div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs" :class="clientRules.requiredFields[field.key] ? 'text-success-600' : 'text-neutral-500'" x-text="clientRules.requiredFields[field.key] ? '必填' : '选填'"></span>
|
||||
<button type="button" class="relative inline-flex h-6 w-11 items-center rounded-full transition"
|
||||
:class="clientRules.requiredFields[field.key] ? 'bg-primary-600' : 'bg-neutral-300'"
|
||||
@click="toggleClientRequired(field)">
|
||||
<span class="inline-block h-5 w-5 transform rounded-full bg-white transition"
|
||||
:class="clientRules.requiredFields[field.key] ? 'translate-x-5' : 'translate-x-1'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md bg-info-50 border border-info-600/20 px-3 py-2 text-xs text-info-600">
|
||||
保存后触发缓存失效:`{tenant_schema}:setting:client_rules`,经纪人下次打开录入页即读取新规则。
|
||||
</div>
|
||||
|
||||
<template x-if="clientRuleError">
|
||||
<div class="rounded-md bg-danger-50 border border-danger-600/20 px-3 py-2 text-xs text-danger-600" x-text="clientRuleError"></div>
|
||||
</template>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button class="px-4 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="saveClientRules()">保存客源规则</button>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 参数编辑弹窗 -->
|
||||
<div x-cloak x-show="lookupEditor.open" class="fixed inset-0 z-40">
|
||||
<div class="absolute inset-0 bg-neutral-900/40" @click="closeLookupEditor()"></div>
|
||||
<div class="absolute inset-0 flex items-center justify-center p-4 pointer-events-none">
|
||||
<section class="w-full max-w-5xl bg-white rounded-xl shadow-xl border border-neutral-200 pointer-events-auto flex flex-col max-h-[90vh]">
|
||||
<header class="px-5 py-4 border-b border-neutral-200 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold" x-text="lookupEditor.title"></h3>
|
||||
<p class="text-xs text-neutral-500 mt-1">支持新增、排序、停用;系统预制项不可删除</p>
|
||||
</div>
|
||||
<button class="p-1.5 rounded-md text-neutral-500 hover:bg-neutral-100" @click="closeLookupEditor()">✕</button>
|
||||
</header>
|
||||
|
||||
<div class="px-5 py-4 overflow-y-auto space-y-3">
|
||||
<template x-if="lookupEditor.error">
|
||||
<div class="rounded-md bg-danger-50 border border-danger-600/20 px-3 py-2 text-xs text-danger-600" x-text="lookupEditor.error"></div>
|
||||
</template>
|
||||
|
||||
<div class="border border-neutral-200 rounded-lg overflow-hidden">
|
||||
<table class="min-w-full text-xs">
|
||||
<thead class="bg-neutral-50 border-b border-neutral-200 text-neutral-500">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">项目值</th>
|
||||
<th class="px-3 py-2 text-left">状态</th>
|
||||
<th class="px-3 py-2 text-left">来源</th>
|
||||
<th class="px-3 py-2 text-left">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-neutral-100 bg-white">
|
||||
<template x-for="(item, idx) in lookupEditor.items" :key="item.id">
|
||||
<tr>
|
||||
<td class="px-3 py-2">
|
||||
<input type="text" x-model.trim="item.label" class="w-full px-2 py-1.5 rounded border border-neutral-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/30" placeholder="请输入项目值" />
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<button type="button" class="relative inline-flex h-6 w-11 items-center rounded-full transition"
|
||||
:class="item.active ? 'bg-primary-600' : 'bg-neutral-300'"
|
||||
@click="item.active = !item.active">
|
||||
<span class="inline-block h-5 w-5 transform rounded-full bg-white transition" :class="item.active ? 'translate-x-5' : 'translate-x-1'"></span>
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px]" :class="item.isSystem ? 'bg-info-50 text-info-600' : 'bg-warning-50 text-warning-600'" x-text="item.isSystem ? '系统预制' : '自定义'"></span>
|
||||
</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">
|
||||
<button class="action-link" :class="idx===0 ? 'opacity-40 pointer-events-none' : ''" @click="moveLookupEditorItem(idx, -1)">上移</button>
|
||||
<button class="action-link ml-2" :class="idx===lookupEditor.items.length-1 ? 'opacity-40 pointer-events-none' : ''" @click="moveLookupEditorItem(idx, 1)">下移</button>
|
||||
<button class="action-link ml-2" :class="item.isSystem ? 'opacity-40 pointer-events-none' : ''" @click="removeLookupEditorItem(idx)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button class="px-3 py-1.5 rounded-md border border-primary-600 text-primary-600 hover:bg-primary-50" @click="addLookupEditorItem()">+ 添加项目</button>
|
||||
</div>
|
||||
|
||||
<footer class="px-5 py-3 border-t border-neutral-200 bg-neutral-50 flex items-center justify-end gap-2">
|
||||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 hover:bg-white" @click="closeLookupEditor()">取消</button>
|
||||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="saveLookupEditor()">保存</button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字段规则编辑抽屉 -->
|
||||
<div x-cloak x-show="ruleDrawer.open" class="fixed inset-0 z-50">
|
||||
<div class="absolute inset-0 bg-neutral-900/40" @click="closeRuleDrawer()"></div>
|
||||
<aside class="absolute right-0 top-0 h-full w-[860px] bg-white border-l border-neutral-200 shadow-2xl flex flex-col">
|
||||
<header class="px-5 py-4 border-b border-neutral-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold" x-text="ruleDrawer.title"></h3>
|
||||
<p class="text-xs text-neutral-500 mt-1">三态规则:必填 / 选填 / 隐藏(隐藏=录入页不展示)</p>
|
||||
</div>
|
||||
<button class="p-1.5 rounded-md text-neutral-500 hover:bg-neutral-100" @click="closeRuleDrawer()">✕</button>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<input x-model.trim="ruleDrawer.search" type="text" placeholder="搜索字段名称" class="w-full px-3 py-2 rounded-md border border-neutral-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/30" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-5 space-y-3">
|
||||
<template x-for="(rule, idx) in filteredRuleDrawerItems" :key="rule.fieldKey">
|
||||
<div class="border border-neutral-200 rounded-lg p-3 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-medium text-sm" x-text="rule.fieldName"></div>
|
||||
<div class="text-xs text-neutral-500" x-text="rule.fieldKey"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 text-xs">
|
||||
<label class="inline-flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" :name="`req-${rule.fieldKey}`" :checked="rule.requirement === 'required'" @change="setDrawerRequirement(rule.fieldKey, 'required')" />
|
||||
<span>必填</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" :name="`req-${rule.fieldKey}`" :checked="rule.requirement === 'optional'" @change="setDrawerRequirement(rule.fieldKey, 'optional')" />
|
||||
<span>选填</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" :name="`req-${rule.fieldKey}`" :checked="rule.requirement === 'hidden'" @change="setDrawerRequirement(rule.fieldKey, 'hidden')" />
|
||||
<span>隐藏</span>
|
||||
</label>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<span class="text-neutral-500">录入页展示</span>
|
||||
<button type="button" class="relative inline-flex h-6 w-11 items-center rounded-full transition"
|
||||
:class="rule.requirement === 'hidden' ? 'bg-neutral-300' : 'bg-primary-600'"
|
||||
@click="toggleDrawerFieldDisplay(rule.fieldKey)">
|
||||
<span class="inline-block h-5 w-5 transform rounded-full bg-white transition"
|
||||
:class="rule.requirement === 'hidden' ? 'translate-x-1' : 'translate-x-5'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="filteredRuleDrawerItems.length === 0">
|
||||
<div class="border border-dashed border-neutral-300 rounded-lg p-8 text-center text-neutral-400">暂无匹配字段</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<footer class="px-5 py-3 border-t border-neutral-200 bg-neutral-50 flex items-center justify-end gap-2">
|
||||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 hover:bg-white" @click="closeRuleDrawer()">取消</button>
|
||||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="saveRuleDrawer()">保存规则</button>
|
||||
</footer>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div x-cloak x-show="toast.show" x-transition class="fixed top-20 right-6 z-[60]">
|
||||
<div class="rounded-lg border px-4 py-2 shadow-lg text-sm"
|
||||
:class="toast.type === 'error' ? 'bg-danger-50 border-danger-600/30 text-danger-600' : 'bg-success-50 border-success-600/30 text-success-600'"
|
||||
x-text="toast.message"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function settingPage() {
|
||||
return {
|
||||
mainTab: 'lookup',
|
||||
toast: { show: false, type: 'success', message: '' },
|
||||
|
||||
lookupFilters: { keyword: '', module: '' },
|
||||
lookupGroups: [],
|
||||
lookupEditor: { open: false, groupId: null, title: '', items: [], error: '' },
|
||||
|
||||
fieldFilters: { keyword: '', entityType: '', tradeStatus: '' },
|
||||
fieldCombos: [],
|
||||
ruleDrawer: { open: false, comboId: null, title: '', items: [], search: '' },
|
||||
|
||||
duplicateScopeOptions: [
|
||||
{ value: 'self', label: '本人(默认)', desc: '同一经纪人不可重复录入同一手机号。' },
|
||||
{ value: 'dept', label: '本部门', desc: '同部门范围内不可重复录入同一手机号。' },
|
||||
{ value: 'company', label: '全公司', desc: '全公司范围内不可重复录入同一手机号。' }
|
||||
],
|
||||
clientRequiredFieldOptions: [
|
||||
{ key: 'grade', label: '等级', desc: '客源等级字段,默认必填且不可关闭。', locked: true },
|
||||
{ key: 'source', label: '来源', desc: '客源来源字段,默认必填且不可关闭。', locked: true },
|
||||
{ key: 'budget_range', label: '求购/求租总价区间', desc: '控制预算区间是否必填。', locked: false },
|
||||
{ key: 'room_requirement', label: '居室需求', desc: '控制户型需求是否必填。', locked: false },
|
||||
{ key: 'buying_purpose', label: '购房目的', desc: '控制购房目的是否必填。', locked: false }
|
||||
],
|
||||
clientRules: {
|
||||
duplicateScope: 'self',
|
||||
requiredFields: {
|
||||
grade: true,
|
||||
source: true,
|
||||
budget_range: false,
|
||||
room_requirement: false,
|
||||
buying_purpose: false
|
||||
}
|
||||
},
|
||||
clientRuleError: '',
|
||||
|
||||
init() {
|
||||
this.seedLookupGroups();
|
||||
this.seedFieldCombos();
|
||||
},
|
||||
|
||||
seedLookupGroups() {
|
||||
this.lookupGroups = [
|
||||
{
|
||||
id: 'g-client-source',
|
||||
module: 'client',
|
||||
moduleLabel: '客源',
|
||||
key: 'source',
|
||||
label: '客源来源',
|
||||
items: [
|
||||
{ id: 'c-src-1', label: '门店接待', sortOrder: 1, active: true, isSystem: true },
|
||||
{ id: 'c-src-2', label: '老客户转介绍', sortOrder: 2, active: true, isSystem: true },
|
||||
{ id: 'c-src-3', label: '驻守派单', sortOrder: 3, active: true, isSystem: true },
|
||||
{ id: 'c-src-4', label: '上门', sortOrder: 4, active: true, isSystem: true },
|
||||
{ id: 'c-src-5', label: '网络-58同城', sortOrder: 5, active: true, isSystem: true },
|
||||
{ id: 'c-src-6', label: '网络-安居客', sortOrder: 6, active: true, isSystem: true },
|
||||
{ id: 'c-src-7', label: '微信', sortOrder: 7, active: true, isSystem: true },
|
||||
{ id: 'c-src-8', label: '抖音线索', sortOrder: 8, active: false, isSystem: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'g-client-purpose',
|
||||
module: 'client',
|
||||
moduleLabel: '客源',
|
||||
key: 'follow_purpose',
|
||||
label: '跟进目的',
|
||||
items: [
|
||||
{ id: 'c-pur-1', label: '回拨', sortOrder: 1, active: true, isSystem: true },
|
||||
{ id: 'c-pur-2', label: '推房', sortOrder: 2, active: true, isSystem: true },
|
||||
{ id: 'c-pur-3', label: '带看', sortOrder: 3, active: true, isSystem: true },
|
||||
{ id: 'c-pur-4', label: '维护', sortOrder: 4, active: true, isSystem: true },
|
||||
{ id: 'c-pur-5', label: '其他', sortOrder: 5, active: true, isSystem: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'g-property-source',
|
||||
module: 'property',
|
||||
moduleLabel: '房源',
|
||||
key: 'source',
|
||||
label: '房源来源',
|
||||
items: [
|
||||
{ id: 'p-src-1', label: '主动开发', sortOrder: 1, active: true, isSystem: true },
|
||||
{ id: 'p-src-2', label: '业主上门', sortOrder: 2, active: true, isSystem: true },
|
||||
{ id: 'p-src-3', label: '老客户转介绍', sortOrder: 3, active: true, isSystem: true },
|
||||
{ id: 'p-src-4', label: '网络来电', sortOrder: 4, active: true, isSystem: true }
|
||||
]
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
seedFieldCombos() {
|
||||
const baseRules = [
|
||||
{ fieldKey: 'orientation', fieldName: '朝向', requirement: 'required' },
|
||||
{ fieldKey: 'decoration', fieldName: '装修情况', requirement: 'required' },
|
||||
{ fieldKey: 'floor', fieldName: '楼层', requirement: 'optional' },
|
||||
{ fieldKey: 'building_area', fieldName: '建筑面积', requirement: 'required' },
|
||||
{ fieldKey: 'inner_area', fieldName: '套内面积', requirement: 'optional' },
|
||||
{ fieldKey: 'room_layout', fieldName: '房型(室/厅/卫)', requirement: 'required' },
|
||||
{ fieldKey: 'ownership_years', fieldName: '产权年限', requirement: 'optional' },
|
||||
{ fieldKey: 'parking_count', fieldName: '车位数', requirement: 'hidden' }
|
||||
];
|
||||
|
||||
this.fieldCombos = [
|
||||
{
|
||||
id: 'res-sale',
|
||||
entityType: 'residential',
|
||||
entityTypeLabel: '住宅',
|
||||
tradeStatus: 'sale',
|
||||
tradeStatusLabel: '出售',
|
||||
updatedAt: '2026-04-29 09:10',
|
||||
rules: this.deepClone(baseRules)
|
||||
},
|
||||
{
|
||||
id: 'res-rent',
|
||||
entityType: 'residential',
|
||||
entityTypeLabel: '住宅',
|
||||
tradeStatus: 'rent',
|
||||
tradeStatusLabel: '出租',
|
||||
updatedAt: '2026-04-29 09:10',
|
||||
rules: this.deepClone(baseRules).map(r => r.fieldKey === 'parking_count' ? { ...r, requirement: 'optional' } : r)
|
||||
},
|
||||
{
|
||||
id: 'shop-sale',
|
||||
entityType: 'shop',
|
||||
entityTypeLabel: '商铺',
|
||||
tradeStatus: 'sale',
|
||||
tradeStatusLabel: '出售',
|
||||
updatedAt: '2026-04-29 09:10',
|
||||
rules: this.deepClone(baseRules).map(r => r.fieldKey === 'room_layout' ? { ...r, requirement: 'hidden' } : r)
|
||||
},
|
||||
{
|
||||
id: 'shop-rent',
|
||||
entityType: 'shop',
|
||||
entityTypeLabel: '商铺',
|
||||
tradeStatus: 'rent',
|
||||
tradeStatusLabel: '出租',
|
||||
updatedAt: '2026-04-29 09:10',
|
||||
rules: this.deepClone(baseRules).map(r => r.fieldKey === 'ownership_years' ? { ...r, requirement: 'hidden' } : r)
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
get filteredLookupGroups() {
|
||||
const keyword = this.lookupFilters.keyword.trim();
|
||||
const module = this.lookupFilters.module;
|
||||
|
||||
return this.lookupGroups
|
||||
.filter(group => !module || group.module === module)
|
||||
.map(group => {
|
||||
if (!keyword) return group;
|
||||
const matchedItems = group.items.filter(item => item.label.includes(keyword));
|
||||
const groupMatched = group.label.includes(keyword) || `${group.module}.${group.key}`.includes(keyword);
|
||||
return {
|
||||
...group,
|
||||
items: groupMatched ? group.items : matchedItems
|
||||
};
|
||||
})
|
||||
.filter(group => keyword ? (group.items.length > 0 || group.label.includes(keyword)) : true);
|
||||
},
|
||||
|
||||
openLookupEditor(group) {
|
||||
this.lookupEditor.open = true;
|
||||
this.lookupEditor.groupId = group.id;
|
||||
this.lookupEditor.title = `${group.label}(${group.module}.${group.key})`;
|
||||
this.lookupEditor.error = '';
|
||||
this.lookupEditor.items = this.deepClone(group.items).sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
},
|
||||
|
||||
closeLookupEditor() {
|
||||
this.lookupEditor = { open: false, groupId: null, title: '', items: [], error: '' };
|
||||
},
|
||||
|
||||
addLookupEditorItem() {
|
||||
this.lookupEditor.items.push({
|
||||
id: `tmp-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
||||
label: '',
|
||||
sortOrder: this.lookupEditor.items.length + 1,
|
||||
active: true,
|
||||
isSystem: false
|
||||
});
|
||||
},
|
||||
|
||||
moveLookupEditorItem(index, direction) {
|
||||
const target = index + direction;
|
||||
if (target < 0 || target >= this.lookupEditor.items.length) return;
|
||||
const cloned = this.lookupEditor.items;
|
||||
[cloned[index], cloned[target]] = [cloned[target], cloned[index]];
|
||||
this.lookupEditor.items = [...cloned];
|
||||
},
|
||||
|
||||
removeLookupEditorItem(index) {
|
||||
const item = this.lookupEditor.items[index];
|
||||
if (item.isSystem) {
|
||||
this.lookupEditor.error = '系统预制项不可删除,仅可停用';
|
||||
return;
|
||||
}
|
||||
this.lookupEditor.items.splice(index, 1);
|
||||
this.lookupEditor.items = [...this.lookupEditor.items];
|
||||
},
|
||||
|
||||
saveLookupEditor() {
|
||||
this.lookupEditor.error = '';
|
||||
const labels = this.lookupEditor.items.map(item => (item.label || '').trim());
|
||||
if (labels.some(v => !v)) {
|
||||
this.lookupEditor.error = '项目值不能为空';
|
||||
return;
|
||||
}
|
||||
const normalized = labels.map(v => v.toLowerCase());
|
||||
if (new Set(normalized).size !== normalized.length) {
|
||||
this.lookupEditor.error = '项目值不可重复';
|
||||
return;
|
||||
}
|
||||
|
||||
this.lookupEditor.items.forEach((item, idx) => {
|
||||
item.label = item.label.trim();
|
||||
item.sortOrder = idx + 1;
|
||||
});
|
||||
|
||||
this.lookupGroups = this.lookupGroups.map(group => {
|
||||
if (group.id !== this.lookupEditor.groupId) return group;
|
||||
return { ...group, items: this.deepClone(this.lookupEditor.items) };
|
||||
});
|
||||
|
||||
const group = this.lookupGroups.find(g => g.id === this.lookupEditor.groupId);
|
||||
this.closeLookupEditor();
|
||||
this.notify(`参数已保存,缓存失效:{tenant_schema}:setting:lookup:${group.module}.${group.key}`);
|
||||
},
|
||||
|
||||
toggleLookupItemActive(groupId, itemId) {
|
||||
this.lookupGroups = this.lookupGroups.map(group => {
|
||||
if (group.id !== groupId) return group;
|
||||
return {
|
||||
...group,
|
||||
items: group.items.map(item => item.id === itemId ? { ...item, active: !item.active } : item)
|
||||
};
|
||||
});
|
||||
this.notify('项目状态已更新');
|
||||
},
|
||||
|
||||
deleteLookupItem(groupId, itemId) {
|
||||
const group = this.lookupGroups.find(g => g.id === groupId);
|
||||
const item = group.items.find(i => i.id === itemId);
|
||||
if (!item || item.isSystem) {
|
||||
this.notify('系统预制项不可删除,仅可停用', 'error');
|
||||
return;
|
||||
}
|
||||
this.lookupGroups = this.lookupGroups.map(g => {
|
||||
if (g.id !== groupId) return g;
|
||||
const nextItems = g.items.filter(i => i.id !== itemId).map((i, idx) => ({ ...i, sortOrder: idx + 1 }));
|
||||
return { ...g, items: nextItems };
|
||||
});
|
||||
this.notify('项目已删除');
|
||||
},
|
||||
|
||||
get filteredFieldCombos() {
|
||||
const keyword = this.fieldFilters.keyword.trim();
|
||||
return this.fieldCombos.filter(combo => {
|
||||
const byEntity = !this.fieldFilters.entityType || combo.entityType === this.fieldFilters.entityType;
|
||||
const byTrade = !this.fieldFilters.tradeStatus || combo.tradeStatus === this.fieldFilters.tradeStatus;
|
||||
const byKeyword = !keyword || combo.entityTypeLabel.includes(keyword) || combo.tradeStatusLabel.includes(keyword);
|
||||
return byEntity && byTrade && byKeyword;
|
||||
});
|
||||
},
|
||||
|
||||
countRequirement(combo, requirement) {
|
||||
return combo.rules.filter(rule => rule.requirement === requirement).length;
|
||||
},
|
||||
|
||||
openRuleDrawer(combo) {
|
||||
this.ruleDrawer.open = true;
|
||||
this.ruleDrawer.comboId = combo.id;
|
||||
this.ruleDrawer.title = `${combo.entityTypeLabel} × ${combo.tradeStatusLabel} 字段规则`;
|
||||
this.ruleDrawer.items = this.deepClone(combo.rules);
|
||||
this.ruleDrawer.search = '';
|
||||
},
|
||||
|
||||
closeRuleDrawer() {
|
||||
this.ruleDrawer = { open: false, comboId: null, title: '', items: [], search: '' };
|
||||
},
|
||||
|
||||
get filteredRuleDrawerItems() {
|
||||
const q = this.ruleDrawer.search.trim();
|
||||
if (!q) return this.ruleDrawer.items;
|
||||
return this.ruleDrawer.items.filter(item => item.fieldName.includes(q) || item.fieldKey.includes(q));
|
||||
},
|
||||
|
||||
setDrawerRequirement(fieldKey, requirement) {
|
||||
this.ruleDrawer.items = this.ruleDrawer.items.map(item => item.fieldKey === fieldKey ? { ...item, requirement } : item);
|
||||
},
|
||||
|
||||
toggleDrawerFieldDisplay(fieldKey) {
|
||||
this.ruleDrawer.items = this.ruleDrawer.items.map(item => {
|
||||
if (item.fieldKey !== fieldKey) return item;
|
||||
if (item.requirement === 'hidden') return { ...item, requirement: 'optional' };
|
||||
return { ...item, requirement: 'hidden' };
|
||||
});
|
||||
},
|
||||
|
||||
saveRuleDrawer() {
|
||||
const now = this.formatNow();
|
||||
this.fieldCombos = this.fieldCombos.map(combo => {
|
||||
if (combo.id !== this.ruleDrawer.comboId) return combo;
|
||||
return { ...combo, rules: this.deepClone(this.ruleDrawer.items), updatedAt: now };
|
||||
});
|
||||
|
||||
const combo = this.fieldCombos.find(c => c.id === this.ruleDrawer.comboId);
|
||||
this.closeRuleDrawer();
|
||||
this.notify(`字段规则已保存,缓存失效:{tenant_schema}:setting:field_req:property.${combo.entityType}.${combo.tradeStatus}`);
|
||||
},
|
||||
|
||||
toggleClientRequired(field) {
|
||||
this.clientRuleError = '';
|
||||
if (field.locked) {
|
||||
this.clientRuleError = '等级、来源为默认必填字段,不可取消';
|
||||
return;
|
||||
}
|
||||
this.clientRules.requiredFields[field.key] = !this.clientRules.requiredFields[field.key];
|
||||
},
|
||||
|
||||
resetClientRules() {
|
||||
this.clientRuleError = '';
|
||||
this.clientRules = {
|
||||
duplicateScope: 'self',
|
||||
requiredFields: {
|
||||
grade: true,
|
||||
source: true,
|
||||
budget_range: false,
|
||||
room_requirement: false,
|
||||
buying_purpose: false
|
||||
}
|
||||
};
|
||||
this.notify('已恢复默认配置');
|
||||
},
|
||||
|
||||
saveClientRules() {
|
||||
this.clientRuleError = '';
|
||||
if (!this.clientRules.requiredFields.grade || !this.clientRules.requiredFields.source) {
|
||||
this.clientRuleError = '等级、来源为默认必填字段,不可取消';
|
||||
return;
|
||||
}
|
||||
this.notify('客源规则已保存,缓存失效:{tenant_schema}:setting:client_rules');
|
||||
},
|
||||
|
||||
closeAllPanels() {
|
||||
if (this.ruleDrawer.open) return this.closeRuleDrawer();
|
||||
if (this.lookupEditor.open) return this.closeLookupEditor();
|
||||
},
|
||||
|
||||
notify(message, type = 'success') {
|
||||
this.toast = { show: true, type, message };
|
||||
setTimeout(() => { this.toast.show = false; }, 2200);
|
||||
},
|
||||
|
||||
deepClone(v) {
|
||||
return JSON.parse(JSON.stringify(v));
|
||||
},
|
||||
|
||||
formatNow() {
|
||||
const now = new Date();
|
||||
const yy = now.getFullYear();
|
||||
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(now.getDate()).padStart(2, '0');
|
||||
const hh = String(now.getHours()).padStart(2, '0');
|
||||
const mi = String(now.getMinutes()).padStart(2, '0');
|
||||
return `${yy}-${mm}-${dd} ${hh}:${mi}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1935
Project/fonrey/UI_DESIGN/组织人事_UI.html
Normal file
1935
Project/fonrey/UI_DESIGN/组织人事_UI.html
Normal file
File diff suppressed because it is too large
Load Diff
219
Project/fonrey/UI_DESIGN/组织人事管理/组织人事_UI.md
Normal file
219
Project/fonrey/UI_DESIGN/组织人事管理/组织人事_UI.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# 组织人事 UI 设计文档
|
||||
|
||||
> **任务编号**:07(P0-C)
|
||||
> **覆盖范围**:`US-ORG-001 ~ US-ORG-003`(组织结构维护 / 员工列表维护 / 员工入职账号创建)
|
||||
> **输出文件**:`UI_DESIGN/组织人事_UI.html`
|
||||
> **设计基线**:`UI_SYSTEM/UI_SYSTEM.md`(后台壳层、表格、分页、Modal/Drawer、树形选择器)
|
||||
> **需求依据**:
|
||||
> - `PRD/TASK.md`(US-ORG-001~003)
|
||||
> - `PRD/组织人事管理/组织人事管理模块PRD.md`
|
||||
> - `DATA_MODEL/DATA_MODEL_ORG.md`
|
||||
> **竞品截图参考**:
|
||||
> - `screenshots/组织人事/组织结构/公司组织结构.png`
|
||||
> - `screenshots/组织人事/组织结构/部门新增.png`
|
||||
> - `screenshots/组织人事/组织结构/部门编辑.png`
|
||||
> - `screenshots/组织人事/组织结构/部门详情.png`
|
||||
> - `screenshots/组织人事/组织结构/部门架构图.png`
|
||||
> - `screenshots/组织人事/组织结构/员工信息编辑.png`
|
||||
> - `screenshots/组织人事/组织结构/员工离职.png`
|
||||
> - `screenshots/组织人事/组织结构/员工调动.png`
|
||||
> - `screenshots/组织人事/组织结构/员工通讯录.png`
|
||||
> - `screenshots/组织人事/职务管理/职务管理.png`
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
### 1.1 页面目标
|
||||
|
||||
组织人事页面用于承载 3 个 P0 核心能力:
|
||||
|
||||
1. **组织结构维护(US-ORG-001)**
|
||||
- 左侧组织树支持新增/编辑/删除部门
|
||||
- 删除有员工部门时阻断并提示
|
||||
2. **员工列表维护(US-ORG-002)**
|
||||
- 支持姓名/工号/手机号关键词检索
|
||||
- 支持部门/状态筛选
|
||||
- 员工姓名可进入员工详情
|
||||
3. **员工入职与账号创建(US-ORG-003)**
|
||||
- 入职表单必填校验(姓名/手机号/所属门店/职位)
|
||||
- 创建后自动生成初始密码并给出发送提示
|
||||
- 新员工立即进入组织数据并可用于后续选择器
|
||||
|
||||
### 1.2 本任务边界
|
||||
|
||||
- ✅ 包含:壳层、组织树、员工表格、架构图视图、通讯录视图、部门弹窗、入职抽屉、离职弹窗、调动抽屉、员工详情抽屉、异动记录弹窗
|
||||
- ✅ 包含:关键必填校验、批量操作启用态、分页演示、Toast 反馈
|
||||
- ⛔ 不包含:真实后端接口、真实地图服务、真实短信发送、真实权限系统
|
||||
|
||||
---
|
||||
|
||||
## 2. 信息架构
|
||||
|
||||
### 2.1 统一壳层
|
||||
|
||||
- **Top Bar(56px)**:品牌 + 一级导航 + 用户区
|
||||
- **Sidebar(240px)**:组织人事二级导航
|
||||
- **Main Content(`ml-60 pt-[72px]`)**:
|
||||
1. 面包屑 + 页面标题
|
||||
2. 主 Tab(组织结构 / 部门架构图 / 员工通讯录)
|
||||
3. 各视图内容
|
||||
|
||||
### 2.2 组织结构视图(默认)
|
||||
|
||||
#### 左栏:组织树
|
||||
- 新增部门按钮
|
||||
- 部门搜索
|
||||
- 显示已关闭部门复选
|
||||
- 树节点(名称 + 在职人数)
|
||||
- 行内操作:编辑 / 删除
|
||||
|
||||
#### 右栏:员工管理
|
||||
1. 顶部预警条(账号上限、证件不匹配)
|
||||
2. 筛选区(关键词、职务、状态、审批状态、入职时间、部门级别等)
|
||||
3. 操作区(新增员工、导出员工、批量调动、批量设置上级、更多、员工异动记录)
|
||||
4. 员工表格(姓名、工号、职务、部门、上级、电话、入职时间、审批状态、操作)
|
||||
5. 分页区
|
||||
|
||||
### 2.3 部门架构图视图
|
||||
|
||||
- 顶部筛选:部门下拉、显示已关闭部门
|
||||
- 说明文案:最多 8 层,支持拖拽缩放
|
||||
- 工具栏:放大、缩小、导出、适配、重置
|
||||
- 树状节点卡片:部门名、级别标签、负责人、人数、直属下级
|
||||
|
||||
### 2.4 员工通讯录视图
|
||||
|
||||
- 筛选:部门、职务、生日、关键字
|
||||
- 表格:部门、姓名、职务、性别、生日、电话、分机、邮箱
|
||||
- 电话操作:拨打 / 查看号码
|
||||
- 分页演示
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键交互设计
|
||||
|
||||
### 3.1 组织树维护
|
||||
|
||||
- 点击节点切换右侧员工列表范围
|
||||
- 新增部门弹窗:保存后实时加入树
|
||||
- 编辑部门弹窗:字段预填并支持状态/属性调整
|
||||
- 删除部门:
|
||||
- 若该部门(含下级)仍有员工,阻断并提示
|
||||
- 无员工时删除成功
|
||||
- 组织树变更后,部门下拉/调动选择器实时更新(同页数据源)
|
||||
|
||||
### 3.2 员工列表筛选与批量
|
||||
|
||||
- 关键词支持:姓名 / 工号 / 手机号
|
||||
- 部门筛选 + “显示下属部门员工”开关
|
||||
- 状态筛选(正式/试用/冻结/离职)
|
||||
- 批量按钮在未勾选时禁用,勾选后启用
|
||||
|
||||
### 3.3 新增员工(入职)抽屉
|
||||
|
||||
#### 必填字段
|
||||
- 姓名
|
||||
- 手机号
|
||||
- 所属门店/店组
|
||||
- 职务
|
||||
|
||||
#### 联动逻辑
|
||||
- 选择职务后自动带出:职务类别、默认角色
|
||||
- 仅允许选择门店/店组作为所属部门
|
||||
|
||||
#### 提交结果
|
||||
- 自动生成初始密码
|
||||
- 弹窗展示账号与密码(模拟发送)
|
||||
- 新员工立即进入列表,审批状态置为“入职审”
|
||||
|
||||
### 3.4 员工离职弹窗
|
||||
|
||||
- 顶部展示业务统计(房源数/客源数/营销客数)
|
||||
- 必填:离职日期、离职类型
|
||||
- 可选:备注
|
||||
- 成功后:员工状态改为“离职”,并写入异动记录
|
||||
|
||||
### 3.5 员工调动抽屉
|
||||
|
||||
- “调动前 / 调动后”双列对照
|
||||
- 必填:调动日期、部门、职务、角色、直属上级(或勾选无直属上级)
|
||||
- 备注最多 30 字
|
||||
- 调动成功后更新员工归属并写入异动记录
|
||||
|
||||
### 3.6 员工详情抽屉
|
||||
|
||||
- 左侧:头像、姓名、部门、职务、工号
|
||||
- 右侧 Tab:员工基本信息 / 异动记录 / 账号信息
|
||||
- 列表点击姓名触发打开
|
||||
|
||||
---
|
||||
|
||||
## 4. 状态矩阵
|
||||
|
||||
| 状态 | 触发 | 页面反馈 |
|
||||
|---|---|---|
|
||||
| 默认态 | 首次进入 | 展示组织结构视图 + 默认部门员工 |
|
||||
| 树节点切换 | 点击部门节点 | 右侧员工列表按节点刷新 |
|
||||
| 查询态 | 输入条件并查询 | 列表过滤结果更新 |
|
||||
| 空结果态 | 条件过严 | 表格空状态“暂无匹配数据” |
|
||||
| 批量勾选态 | 勾选复选框 | 批量按钮启用 + 已选计数 |
|
||||
| 新增员工校验失败 | 缺失必填 | 字段红字错误提示,不提交 |
|
||||
| 部门删除拦截 | 部门有员工 | Toast 提示并阻断删除 |
|
||||
| 离职/调动成功 | 表单校验通过 | 更新列表 + 写异动日志 + Toast |
|
||||
| 主题策略 | 后台统一视觉 | 页面内不包含 Light/Dark/System 切换控件 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 字段与数据模型映射(DATA_MODEL_ORG)
|
||||
|
||||
| UI 字段 | 数据模型字段 |
|
||||
|---|---|
|
||||
| 部门名称 | `org_units.name` |
|
||||
| 部门级别 | `org_units.type` |
|
||||
| 上级部门 | `org_units.parent_id` |
|
||||
| 部门属性 | `org_units.attribute` |
|
||||
| 部门状态 | `org_units.is_active` |
|
||||
| 员工姓名 | `staff.name` |
|
||||
| 员工工号 | `staff.employee_no` |
|
||||
| 职务 | `staff.job_title` |
|
||||
| 职务类别 | `staff.job_category` |
|
||||
| 所属部门 | `staff.org_unit_id` |
|
||||
| 员工状态 | `staff.status` |
|
||||
| 手机号(脱敏显示) | `staff.phone_enc` + 展示脱敏规则 |
|
||||
| 入职日期 | `staff.first_joined_at` |
|
||||
| 离职日期 | `staff.resigned_at` |
|
||||
| 异动类型 | `staff_transfer_logs.transfer_type` |
|
||||
| 异动时间 | `staff_transfer_logs.transfer_date` |
|
||||
| 操作人 | `staff_transfer_logs.operator_id` |
|
||||
| 账号平台绑定 | `staff_accounts.platform / is_bound` |
|
||||
|
||||
---
|
||||
|
||||
## 6. 可访问性与实现规范
|
||||
|
||||
- 表格列头使用 `<th scope="col">`
|
||||
- 图标按钮补充 `aria-label`
|
||||
- Modal/Drawer 支持 `Esc` 关闭
|
||||
- 必填错误信息使用文本提示,不仅靠颜色
|
||||
- 焦点统一 `focus-visible:ring-2 focus-visible:ring-primary-600/40`
|
||||
- 所有关键操作给出 Toast 反馈
|
||||
- 页面遵循既有后台浅色视觉,不加入主题切换控件
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收清单(本轮)
|
||||
|
||||
- [x] 覆盖 US-ORG-001:组织树增改删 + 删除拦截
|
||||
- [x] 覆盖 US-ORG-002:员工关键词/部门/状态筛选 + 姓名详情入口
|
||||
- [x] 覆盖 US-ORG-003:入职必填校验 + 自动密码 + 成功入列
|
||||
- [x] 竞品关键结构已映射:组织树、架构图、离职弹窗、调动抽屉、通讯录
|
||||
- [x] 页面不含 Light/Dark/System 切换控件
|
||||
- [ ] 控制台 0 报错(待本地预览验证)
|
||||
|
||||
---
|
||||
|
||||
## 8. 后续衔接
|
||||
|
||||
- 本任务评审通过后进入任务 08:权限管理(US-PERMISSION-001~005)
|
||||
- 组织结构与员工数据将作为权限人员列表、角色分配的数据基础
|
||||
536
Project/fonrey/UI_DESIGN/首页设置_UI.html
Normal file
536
Project/fonrey/UI_DESIGN/首页设置_UI.html
Normal file
@@ -0,0 +1,536 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1366" />
|
||||
<title>Fonrey 首页设置 · 静态原型</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#F0FDFA',
|
||||
100: '#CCFBF1',
|
||||
200: '#99F6E4',
|
||||
500: '#14B8A6',
|
||||
600: '#0F766E',
|
||||
700: '#115E59',
|
||||
800: '#134E4A'
|
||||
},
|
||||
neutral: {
|
||||
50: '#F8FAFC',
|
||||
100: '#F1F5F9',
|
||||
200: '#E2E8F0',
|
||||
300: '#CBD5E1',
|
||||
400: '#94A3B8',
|
||||
500: '#64748B',
|
||||
600: '#475569',
|
||||
700: '#334155',
|
||||
800: '#1E293B',
|
||||
900: '#0F172A'
|
||||
},
|
||||
success: { 50: '#F0FDF4', 600: '#16A34A' },
|
||||
warning: { 50: '#FFFBEB', 600: '#D97706' },
|
||||
danger: { 50: '#FEF2F2', 600: '#DC2626' },
|
||||
info: { 50: '#EFF6FF', 600: '#2563EB' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { background: #F8FAFC; color: #0F172A; }
|
||||
[x-cloak] { display: none !important; }
|
||||
.role-tab { color: #64748B; border: 1px solid #E2E8F0; background: #fff; }
|
||||
.role-tab.active { color: #115E59; border-color: #115E59; background: #F0FDFA; font-weight: 600; }
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: 38px;
|
||||
height: 22px;
|
||||
border-radius: 999px;
|
||||
background: #CBD5E1;
|
||||
transition: background .2s;
|
||||
}
|
||||
.switch::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
background: #fff;
|
||||
transition: transform .2s;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.2);
|
||||
}
|
||||
.switch.on { background: #0F766E; }
|
||||
.switch.on::after { transform: translateX(16px); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="text-sm antialiased" x-data="homeSettingPage()" x-init="init()">
|
||||
<header class="fixed top-0 left-0 right-0 h-14 z-30 bg-primary-800 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 px-4 w-60 shrink-0">
|
||||
<div class="w-7 h-7 rounded-md bg-primary-500 flex items-center justify-center text-white text-sm font-semibold">F</div>
|
||||
<span class="text-base font-semibold text-white">Fonrey</span>
|
||||
</div>
|
||||
<nav class="flex items-center gap-1 flex-1 px-2" aria-label="主导航">
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">主页</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">房源</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">客源</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">营销</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">交易</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">数据</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">人事</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md bg-primary-600 text-white font-medium">系统</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">三网</a>
|
||||
</nav>
|
||||
<div class="flex items-center gap-1 px-4 shrink-0">
|
||||
<button class="p-1.5 text-primary-200 hover:bg-primary-700 rounded-md" aria-label="消息">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022 23.848 23.848 0 0 0 5.454 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 pl-3 ml-1 border-l border-primary-700">
|
||||
<div class="w-8 h-8 rounded-full bg-primary-600 text-white flex items-center justify-center text-sm font-semibold">杜</div>
|
||||
<span class="text-sm font-medium text-primary-100">杜利强</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<aside class="fixed left-0 top-14 h-[calc(100vh-56px)] w-60 z-20 border-r border-neutral-200 bg-white overflow-y-auto">
|
||||
<nav class="p-3 space-y-0.5">
|
||||
<div class="px-2 pt-2 pb-1 text-xs font-medium text-neutral-500 uppercase tracking-wide">设置</div>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md bg-primary-50 text-primary-700 font-medium">首页设置</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">房源设置</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">新房设置</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">客源设置</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">交易设置</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">人事OA设置</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="ml-60 pt-[72px] min-h-screen px-6 py-5">
|
||||
<div class="mx-auto max-w-[1760px] space-y-4">
|
||||
<section class="bg-white border border-neutral-200 rounded-lg p-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<nav class="flex items-center gap-1 text-xs text-neutral-500 mb-2" aria-label="面包屑">
|
||||
<a href="#" class="hover:text-neutral-700">系统</a>
|
||||
<span>/</span>
|
||||
<a href="#" class="hover:text-neutral-700">设置</a>
|
||||
<span>/</span>
|
||||
<span class="text-neutral-900">首页设置</span>
|
||||
</nav>
|
||||
<h1 class="text-xl font-semibold text-neutral-900">系统配置-首页设置</h1>
|
||||
<p class="text-xs text-neutral-500 mt-1">管理员可按角色配置首页展示卡片、排行榜及成交战报视图</p>
|
||||
</div>
|
||||
<div class="w-[320px]">
|
||||
<input type="text" x-model.trim="searchKeyword" placeholder="请输入设置项名称" class="w-full px-3 py-2 rounded-md border border-neutral-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/30" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-12 gap-4 items-start">
|
||||
<section class="col-span-8 space-y-4">
|
||||
<section class="bg-white border border-neutral-200 rounded-lg p-4 space-y-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2" role="tablist" aria-label="角色视图切换">
|
||||
<template x-for="role in roleOptions" :key="role.key">
|
||||
<button
|
||||
class="role-tab px-3 py-1.5 rounded-md"
|
||||
:class="{ 'active': currentRole === role.key }"
|
||||
@click="switchRole(role.key)"
|
||||
x-text="role.label"
|
||||
></button>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<template x-if="!editMode">
|
||||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="startEdit">编辑</button>
|
||||
</template>
|
||||
<template x-if="editMode">
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="cancelEdit">取消</button>
|
||||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="saveConfig">保存配置</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="formError" class="rounded-md border border-danger-600 bg-danger-50 text-danger-600 px-3 py-2 text-xs" x-text="formError"></div>
|
||||
|
||||
<section class="rounded-lg border border-neutral-200" x-show="matchSection('员工信息')">
|
||||
<header class="px-4 py-3 border-b border-neutral-200 bg-neutral-50 font-semibold">员工信息模块</header>
|
||||
<div class="p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p class="font-medium">是否展示员工司龄</p>
|
||||
<p class="text-xs text-neutral-500 mt-1">若开启则首页展示员工司龄</p>
|
||||
</div>
|
||||
<button class="switch" :class="draftConfig.show_seniority ? 'on' : ''" @click="toggleBoolean('show_seniority')" :disabled="!editMode"></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-lg border border-neutral-200" x-show="matchSection('行程')">
|
||||
<header class="px-4 py-3 border-b border-neutral-200 bg-neutral-50 font-semibold">行程模块显示指标</header>
|
||||
<div class="p-4 space-y-2">
|
||||
<label class="inline-flex items-center gap-2 mr-4">
|
||||
<input type="checkbox" class="rounded border-neutral-300" :checked="draftConfig.itinerary_metrics.includes('sale')" @change="toggleMetric('sale')" :disabled="!editMode" />
|
||||
<span>买卖</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="checkbox" class="rounded border-neutral-300" :checked="draftConfig.itinerary_metrics.includes('rent')" @change="toggleMetric('rent')" :disabled="!editMode" />
|
||||
<span>租赁</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-lg border border-neutral-200" x-show="matchSection('业绩')">
|
||||
<header class="px-4 py-3 border-b border-neutral-200 bg-neutral-50 font-semibold">首页业绩显示设置</header>
|
||||
<div class="p-4 space-y-3">
|
||||
<label class="flex items-start gap-2">
|
||||
<input type="radio" name="kpi_mode" value="pending_and_approved" :checked="draftConfig.kpi_mode==='pending_and_approved'" @change="draftConfig.kpi_mode='pending_and_approved'" :disabled="!editMode" class="mt-0.5" />
|
||||
<span class="text-xs leading-5">统计审批中和审批通过的转定业绩:录转定/认购后统计审批中、审批驳回和审批通过的业绩,若无转定/认购,直接签约则统计录签约后审批中、审批驳回和审批通过的分成后业绩</span>
|
||||
</label>
|
||||
<label class="flex items-start gap-2">
|
||||
<input type="radio" name="kpi_mode" value="approved_only" :checked="draftConfig.kpi_mode==='approved_only'" @change="draftConfig.kpi_mode='approved_only'" :disabled="!editMode" class="mt-0.5" />
|
||||
<span class="text-xs leading-5">仅统计审批通过业绩(严格口径)</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-lg border border-neutral-200" x-show="matchSection('统计卡片')">
|
||||
<header class="px-4 py-3 border-b border-neutral-200 bg-neutral-50 font-semibold">首页统计卡片配置</header>
|
||||
<div class="p-4 space-y-2">
|
||||
<template x-for="card in sortedCards" :key="card.key">
|
||||
<div class="border border-neutral-200 rounded-md px-3 py-2 flex items-center justify-between gap-3 bg-white">
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="switch" :class="card.enabled ? 'on' : ''" @click="toggleCard(card.key)" :disabled="!editMode"></button>
|
||||
<div>
|
||||
<p class="font-medium" x-text="card.label"></p>
|
||||
<p class="text-xs text-neutral-500" x-text="card.desc"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<button class="px-2 py-1 rounded border border-neutral-300 text-xs" @click="moveCard(card.key,-1)" :disabled="!editMode">上移</button>
|
||||
<button class="px-2 py-1 rounded border border-neutral-300 text-xs" @click="moveCard(card.key,1)" :disabled="!editMode">下移</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<p class="text-xs text-neutral-500 mt-1">至少保留1个启用卡片;卡片顺序将影响首页展示顺序。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-lg border border-neutral-200" x-show="matchSection('排行榜')">
|
||||
<header class="px-4 py-3 border-b border-neutral-200 bg-neutral-50 font-semibold">排行榜设置</header>
|
||||
<div class="p-4 grid grid-cols-2 gap-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="font-medium">是否显示业绩和单数</p>
|
||||
<p class="text-xs text-neutral-500">若开启则排行榜中显示业绩和单数</p>
|
||||
</div>
|
||||
<button class="switch" :class="draftConfig.ranking.show_performance_and_deal ? 'on' : ''" @click="toggleNested('ranking','show_performance_and_deal')" :disabled="!editMode"></button>
|
||||
</div>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-xs text-neutral-500">业绩计算方式</span>
|
||||
<select class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300 bg-white" x-model="draftConfig.ranking.calc_mode" :disabled="!editMode">
|
||||
<option value="turn_order">统计审批中和审批通过的转定单量和业绩</option>
|
||||
<option value="approved_only">仅统计审批通过单量和业绩</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-xs text-neutral-500">默认按店或组排名</span>
|
||||
<select class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300 bg-white" x-model="draftConfig.ranking.default_rank_level" :disabled="!editMode">
|
||||
<option value="store">门店</option>
|
||||
<option value="group">组别</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="font-medium">默认展示全公司前10排名数据</p>
|
||||
<p class="text-xs text-neutral-500">权限为本人/无时仅展示默认排行</p>
|
||||
</div>
|
||||
<button class="switch" :class="draftConfig.ranking.show_company_top10 ? 'on' : ''" @click="toggleNested('ranking','show_company_top10')" :disabled="!editMode"></button>
|
||||
</div>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-xs text-neutral-500">过滤账号</span>
|
||||
<input type="text" class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300" placeholder="无限制" x-model="draftConfig.ranking.filter_accounts" :disabled="!editMode" />
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-xs text-neutral-500">过滤部门</span>
|
||||
<input type="text" class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300" placeholder="无限制" x-model="draftConfig.ranking.filter_departments" :disabled="!editMode" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-lg border border-neutral-200" x-show="matchSection('成交战报')">
|
||||
<header class="px-4 py-3 border-b border-neutral-200 bg-neutral-50 font-semibold">成交战报设置</header>
|
||||
<div class="p-4 grid grid-cols-2 gap-4">
|
||||
<div class="space-y-3">
|
||||
<label class="block">
|
||||
<span class="text-xs text-neutral-500">成交时间范围</span>
|
||||
<input type="text" class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300" placeholder="不限制" x-model="draftConfig.battle_report.days_range" :disabled="!editMode" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-xs text-neutral-500">成交业绩范围</span>
|
||||
<input type="text" class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300" placeholder="不限制" x-model="draftConfig.battle_report.amount_range" :disabled="!editMode" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div><p class="font-medium">是否显示业绩</p><p class="text-xs text-neutral-500">若开启则成交战报中显示业绩</p></div>
|
||||
<button class="switch" :class="draftConfig.battle_report.show_performance ? 'on' : ''" @click="toggleNested('battle_report','show_performance')" :disabled="!editMode"></button>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div><p class="font-medium">是否显示房源</p><p class="text-xs text-neutral-500">若开启则成交战报中显示房源名称</p></div>
|
||||
<button class="switch" :class="draftConfig.battle_report.show_property_name ? 'on' : ''" @click="toggleNested('battle_report','show_property_name')" :disabled="!editMode"></button>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div><p class="font-medium">是否显示房源总价</p><p class="text-xs text-neutral-500">若开启则成交战报中显示房源总价</p></div>
|
||||
<button class="switch" :class="draftConfig.battle_report.show_property_total_price ? 'on' : ''" @click="toggleNested('battle_report','show_property_total_price')" :disabled="!editMode"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<aside class="col-span-4 space-y-4 sticky top-[84px]">
|
||||
<section class="bg-white border border-neutral-200 rounded-lg p-4">
|
||||
<h3 class="text-base font-semibold mb-3">首页预览(<span x-text="currentRoleLabel"></span>)</h3>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<template x-for="card in enabledCards" :key="card.key">
|
||||
<div class="rounded-md border border-neutral-200 p-3 bg-neutral-50">
|
||||
<p class="text-xs text-neutral-500" x-text="card.label"></p>
|
||||
<p class="text-lg font-semibold mt-1" x-text="metricValue(card.key)"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-white border border-neutral-200 rounded-lg p-4 text-xs text-neutral-600 space-y-2">
|
||||
<p><span class="font-medium text-neutral-800">员工司龄:</span><span x-text="draftConfig.show_seniority ? '显示' : '隐藏'"></span></p>
|
||||
<p><span class="font-medium text-neutral-800">行程指标:</span><span x-text="draftConfig.itinerary_metrics.length ? draftConfig.itinerary_metrics.map(v => v==='sale'?'买卖':'租赁').join('、') : '无' "></span></p>
|
||||
<p><span class="font-medium text-neutral-800">排行榜显示业绩/单数:</span><span x-text="draftConfig.ranking.show_performance_and_deal ? '开启' : '关闭'"></span></p>
|
||||
<p><span class="font-medium text-neutral-800">成交战报展示房源:</span><span x-text="draftConfig.battle_report.show_property_name ? '开启' : '关闭'"></span></p>
|
||||
</section>
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div x-cloak x-show="toast.open" x-transition class="fixed right-6 bottom-6 px-4 py-2 rounded-md shadow-lg text-sm bg-neutral-900 text-white" x-text="toast.message"></div>
|
||||
|
||||
<script>
|
||||
function homeSettingPage() {
|
||||
return {
|
||||
searchKeyword: '',
|
||||
editMode: false,
|
||||
formError: '',
|
||||
currentRole: 'agent',
|
||||
roleOptions: [
|
||||
{ key: 'agent', label: '经纪人视图' },
|
||||
{ key: 'manager', label: '店长视图' },
|
||||
{ key: 'admin', label: '管理员视图' }
|
||||
],
|
||||
roleConfigs: {},
|
||||
draftConfig: null,
|
||||
backupConfig: null,
|
||||
toast: { open: false, message: '' },
|
||||
|
||||
cardCatalog: {
|
||||
new_property_today: { label: '今日新增房源', desc: '统计今日新增房源数量' },
|
||||
new_client_today: { label: '今日新增客源', desc: '统计今日新增客源数量' },
|
||||
new_showing_today: { label: '今日新增带看', desc: '统计今日新增带看数量' },
|
||||
new_followup_today: { label: '今日新增跟进', desc: '统计今日新增跟进数量' },
|
||||
signed_deals_today: { label: '今日签约单量', desc: '统计今日签约套数' },
|
||||
deal_amount_today: { label: '今日成交业绩', desc: '统计今日成交业绩金额' }
|
||||
},
|
||||
|
||||
metricValues: {
|
||||
new_property_today: '18',
|
||||
new_client_today: '12',
|
||||
new_showing_today: '9',
|
||||
new_followup_today: '36',
|
||||
signed_deals_today: '4',
|
||||
deal_amount_today: '126.8万'
|
||||
},
|
||||
|
||||
init() {
|
||||
this.roleConfigs = {
|
||||
agent: this.defaultRoleConfig('agent'),
|
||||
manager: this.defaultRoleConfig('manager'),
|
||||
admin: this.defaultRoleConfig('admin')
|
||||
};
|
||||
this.loadRole('agent');
|
||||
},
|
||||
|
||||
defaultRoleConfig(role) {
|
||||
const baseCards = [
|
||||
{ key: 'new_property_today', enabled: true, sort: 1 },
|
||||
{ key: 'new_client_today', enabled: true, sort: 2 },
|
||||
{ key: 'new_followup_today', enabled: true, sort: 3 },
|
||||
{ key: 'new_showing_today', enabled: role !== 'agent', sort: 4 },
|
||||
{ key: 'signed_deals_today', enabled: role === 'admin' || role === 'manager', sort: 5 },
|
||||
{ key: 'deal_amount_today', enabled: role === 'admin', sort: 6 }
|
||||
];
|
||||
return {
|
||||
show_seniority: role !== 'agent',
|
||||
itinerary_metrics: ['sale', 'rent'],
|
||||
kpi_mode: 'pending_and_approved',
|
||||
home_cards: baseCards,
|
||||
ranking: {
|
||||
show_performance_and_deal: true,
|
||||
calc_mode: 'turn_order',
|
||||
default_rank_level: role === 'agent' ? 'group' : 'store',
|
||||
show_company_top10: role === 'admin',
|
||||
filter_accounts: '',
|
||||
filter_departments: ''
|
||||
},
|
||||
battle_report: {
|
||||
days_range: '',
|
||||
amount_range: '',
|
||||
show_performance: role === 'admin',
|
||||
show_property_name: true,
|
||||
show_property_total_price: role === 'admin'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
get currentRoleLabel() {
|
||||
return this.roleOptions.find(r => r.key === this.currentRole)?.label || '';
|
||||
},
|
||||
|
||||
get sortedCards() {
|
||||
if (!this.draftConfig) return [];
|
||||
return this.draftConfig.home_cards
|
||||
.slice()
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
.map(c => ({ ...c, label: this.cardCatalog[c.key].label, desc: this.cardCatalog[c.key].desc }));
|
||||
},
|
||||
|
||||
get enabledCards() {
|
||||
return this.sortedCards.filter(c => c.enabled);
|
||||
},
|
||||
|
||||
metricValue(key) {
|
||||
return this.metricValues[key] || '-';
|
||||
},
|
||||
|
||||
matchSection(label) {
|
||||
if (!this.searchKeyword) return true;
|
||||
return label.includes(this.searchKeyword);
|
||||
},
|
||||
|
||||
loadRole(role) {
|
||||
this.currentRole = role;
|
||||
this.draftConfig = this.deepClone(this.roleConfigs[role]);
|
||||
this.backupConfig = this.deepClone(this.roleConfigs[role]);
|
||||
this.formError = '';
|
||||
this.editMode = false;
|
||||
},
|
||||
|
||||
switchRole(role) {
|
||||
if (this.editMode) {
|
||||
this.formError = '请先保存或取消当前角色编辑内容';
|
||||
return;
|
||||
}
|
||||
this.loadRole(role);
|
||||
},
|
||||
|
||||
startEdit() {
|
||||
this.editMode = true;
|
||||
this.formError = '';
|
||||
this.backupConfig = this.deepClone(this.draftConfig);
|
||||
},
|
||||
|
||||
cancelEdit() {
|
||||
this.draftConfig = this.deepClone(this.backupConfig);
|
||||
this.editMode = false;
|
||||
this.formError = '';
|
||||
this.notify('已取消编辑,恢复到上次保存状态');
|
||||
},
|
||||
|
||||
saveConfig() {
|
||||
this.formError = '';
|
||||
const enabledCount = this.draftConfig.home_cards.filter(c => c.enabled).length;
|
||||
if (enabledCount < 1) {
|
||||
this.formError = '至少保留 1 个首页统计卡片';
|
||||
return;
|
||||
}
|
||||
if ((this.draftConfig.ranking.filter_accounts || '').length > 100) {
|
||||
this.formError = '过滤账号输入过长,请控制在100字符以内';
|
||||
return;
|
||||
}
|
||||
if ((this.draftConfig.ranking.filter_departments || '').length > 100) {
|
||||
this.formError = '过滤部门输入过长,请控制在100字符以内';
|
||||
return;
|
||||
}
|
||||
|
||||
this.roleConfigs[this.currentRole] = this.deepClone(this.draftConfig);
|
||||
this.backupConfig = this.deepClone(this.draftConfig);
|
||||
this.editMode = false;
|
||||
this.notify('首页设置已保存,当前角色视图即时生效');
|
||||
},
|
||||
|
||||
toggleBoolean(field) {
|
||||
if (!this.editMode) return;
|
||||
this.draftConfig[field] = !this.draftConfig[field];
|
||||
},
|
||||
|
||||
toggleNested(group, field) {
|
||||
if (!this.editMode) return;
|
||||
this.draftConfig[group][field] = !this.draftConfig[group][field];
|
||||
},
|
||||
|
||||
toggleMetric(metric) {
|
||||
if (!this.editMode) return;
|
||||
const list = this.draftConfig.itinerary_metrics;
|
||||
const idx = list.indexOf(metric);
|
||||
if (idx >= 0) list.splice(idx, 1);
|
||||
else list.push(metric);
|
||||
},
|
||||
|
||||
toggleCard(key) {
|
||||
if (!this.editMode) return;
|
||||
const card = this.draftConfig.home_cards.find(c => c.key === key);
|
||||
if (!card) return;
|
||||
card.enabled = !card.enabled;
|
||||
},
|
||||
|
||||
moveCard(key, delta) {
|
||||
if (!this.editMode) return;
|
||||
const cards = this.draftConfig.home_cards.slice().sort((a,b) => a.sort-b.sort);
|
||||
const index = cards.findIndex(c => c.key === key);
|
||||
if (index < 0) return;
|
||||
const target = index + delta;
|
||||
if (target < 0 || target >= cards.length) return;
|
||||
const tmp = cards[index].sort;
|
||||
cards[index].sort = cards[target].sort;
|
||||
cards[target].sort = tmp;
|
||||
},
|
||||
|
||||
notify(message) {
|
||||
this.toast.message = message;
|
||||
this.toast.open = true;
|
||||
setTimeout(() => {
|
||||
this.toast.open = false;
|
||||
}, 1800);
|
||||
},
|
||||
|
||||
deepClone(v) { return JSON.parse(JSON.stringify(v)); }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,6 @@
|
||||
# Fonrey 项目骨架搭建 — 工程执行提示词
|
||||
> **版本**:v2.2(2026-04-28)|v2.0 修复 P0×5+P1×4;v2.1 修复 P0×9(交叉比对 AGENTS.md / 测试规范.md / 系统管理技术文档.md);v2.2 收口剩余一致性问题(URL 分离、Admin 弃用、密钥变量统一、测试 settings 一致性、环境变量占位修复)
|
||||
> **v2.2 主要变更**:统一 `config/urls.py` / `config/urls_public.py` 职责并修正执行清单;移除 Django Admin 路由引用(对齐系统管理技术文档);PII 密钥统一为 `PHONE_ENCRYPTION_KEY`;`pyproject.toml` 测试 settings 对齐 `config.settings.testing` 并新增 `testing.py` 生成要求;修复 AWS/R2 示例占位符与 `.env.example` 断行问题;修正 docker-compose 服务数量描述
|
||||
> **版本**:v2.3(2026-04-29)|v2.0 修复 P0×5+P1×4;v2.1 修复 P0×9(交叉比对 AGENTS.md / 测试规范.md / 系统管理技术文档.md);v2.2 收口剩余一致性问题(URL 分离、Admin 弃用、密钥变量统一、测试 settings 一致性、环境变量占位修复);v2.3 修复开工阻塞(R2 密钥变量名、release 结构冲突、DB 连接参数)并补齐 API_CONTRACT 核对清单
|
||||
> **v2.3 主要变更**:修复 `AWS_SECRET_ACCESS_KEY` 环境变量占位错误(统一为 `R2_SECRET_ACCESS_KEY`);移除 `DATABASES.OPTIONS.pool_size` 非标准参数;修正执行清单中 `apps/release` 误要求 `services/` 与 `tasks.py` 的冲突;补充 API_CONTRACT 强制核对清单(路径/方法/参数/响应 envelope/错误码/@extend_schema/openapi.json/schemathesis);统一 URL 命名口径说明(`config.urls` 对应系统文档 `config.urls_tenant`)
|
||||
## 你的角色与约束
|
||||
你是一名资深 Django 后端工程师。你的任务是**严格按照规范**搭建 Fonrey 项目骨架,不得自行发明技术方案,不得引入文档未授权的第三方库。每一步操作后必须验证结果。
|
||||
**项目工作目录**:`/mnt/c/Project/`(在此目录下创建 `fonrey/` 子目录)
|
||||
@@ -188,7 +188,7 @@ from django.urls import path, include
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||
|
||||
urlpatterns = [
|
||||
path("api/client/updates/latest/", include("apps.release.urls")),
|
||||
path("api/client/", include("apps.release.urls")), # apps.release.urls 内定义 updates/latest/
|
||||
# OpenAPI — 仅 DEBUG 暴露,production 通过 nginx ACL 限制
|
||||
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
|
||||
@@ -200,6 +200,7 @@ urlpatterns = [
|
||||
# config/settings/base.py — 多租户 URL 分离配置(必须显式声明)
|
||||
ROOT_URLCONF = "config.urls" # tenant schema 路由入口
|
||||
PUBLIC_SCHEMA_URLCONF = "config.urls_public" # public schema 路由入口
|
||||
# 口径说明:系统管理技术文档中的 `config.urls_tenant` 在本骨架中命名为 `config.urls`,语义等价。
|
||||
```
|
||||
|
||||
> `config/urls.py` 仅包含 `urlpatterns`(tenant 路由),`config/urls_public.py` 包含 `urlpatterns`(public 路由)。两个文件分开维护,**不得合并**。
|
||||
@@ -255,7 +256,7 @@ DATABASES = {
|
||||
"HOST": env("DB_HOST", default="localhost"),
|
||||
"PORT": env("DB_PORT", default="5432"),
|
||||
"CONN_MAX_AGE": 60,
|
||||
"OPTIONS": {"pool_size": 10}, # PgBouncer 协同
|
||||
# PgBouncer 连接池在 DB/Proxy 层管理;此处不注入非标准 DSN 参数,避免驱动报错
|
||||
}
|
||||
}
|
||||
DATABASE_ROUTERS = ["django_tenants.routers.TenantSyncRouter"]
|
||||
@@ -317,7 +318,7 @@ SPECTACULAR_SETTINGS = {
|
||||
"VERSION": "1.0.0",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"COMPONENT_SPLIT_REQUEST": True,
|
||||
"ENUM_GENERATE_CHOICE_DESCRIPTION": False, # 枚举说明由 ENUMS.md 权威维护
|
||||
"ENUM_GENERATE_CHOICE_DESCRIPTION": True, # 对齐 API_CONTRACT.md §11,Schema 中展开枚举说明
|
||||
}
|
||||
# 日志(骨架,production 扩展)
|
||||
LOGGING = {
|
||||
@@ -785,7 +786,7 @@ CELERY_BROKER_URL=redis://redis:6379/1
|
||||
# Cloudflare R2
|
||||
R2_ENDPOINT_URL=https://<account_id>.r2.cloudflarestorage.com
|
||||
R2_ACCESS_KEY_ID=
|
||||
R2_SECRET_ACCESS_KEY=
|
||||
R2_SECRET_ACCESS_KEY=<your_r2_secret_access_key>
|
||||
R2_BUCKET_NAME=media
|
||||
R2_CUSTOM_DOMAIN=
|
||||
# Sentry(production 填写)
|
||||
@@ -840,7 +841,21 @@ python_files = ["test_*.py", "*_test.py"]
|
||||
addopts = "--reuse-db --cov=apps --cov=core --cov-report=term-missing -n auto"
|
||||
```
|
||||
---
|
||||
## 十五、执行顺序与验证清单
|
||||
## 十五、API_CONTRACT 契约核对清单(强制)
|
||||
在生成任何 API 相关骨架(含 `apps/release`、OpenAPI 路由、契约测试占位)时,必须逐项核对:
|
||||
|
||||
- [ ] **路径与方法**:端点路径/HTTP Method 与 `TECH_STACK/API_CONTRACT.md` 及模块文档一致
|
||||
- [ ] **请求参数**:query/path/body 字段名、类型、必填/可选与契约一致
|
||||
- [ ] **响应 envelope**:成功返回 `ok=true` + `data` + `meta`;失败返回 `ok=false` + `error` + `code` + `details` + `meta`
|
||||
- [ ] **错误码**:`code` 使用稳定 `UPPER_SNAKE_CASE`,并与模块前缀语义一致
|
||||
- [ ] **OpenAPI 注解**:视图补齐 `@extend_schema`(或 `@extend_schema_view`)
|
||||
- [ ] **Schema 文件**:可执行 `python manage.py spectacular --file openapi.json` 成功生成
|
||||
- [ ] **契约测试**:`schemathesis` 命令可运行(至少保留 Positive 路径骨架)
|
||||
|
||||
交付时必须附带“API 契约核对结果”小节,按以上 7 项逐项标注 ✅/❌ 与证据(文件路径 + 行号)。
|
||||
|
||||
---
|
||||
## 十六、执行顺序与验证清单
|
||||
按以下顺序执行,每步完成后打 ✅:
|
||||
```
|
||||
[ ] 1. 创建根目录 fonrey/ 及上述完整目录树(含所有 __init__.py)
|
||||
@@ -857,7 +872,7 @@ addopts = "--reuse-db --cov=apps --cov=core --cov-report=term-missing -n auto"
|
||||
[ ] 11. 创建 core/htmx.py(htmx_response 工具)
|
||||
[ ] 12. 创建 core/templatetags/heroicons.py
|
||||
[ ] 13. 创建 core/middleware/audit.py(骨架)
|
||||
[ ] 14. 为每个 App 创建目录结构(含 apps.py、models/__init__.py、services/__init__.py、tasks.py 骨架、views.py 骨架、urls.py 骨架)
|
||||
[ ] 14. 为每个 App 创建目录结构(`apps/release` 除外;其余含 apps.py、models/__init__.py、services/__init__.py、tasks.py 骨架、views.py 骨架、urls.py 骨架)
|
||||
[ ] 15. 创建 apps/tenant/models.py(Tenant、Domain 模型,django-tenants 规范)
|
||||
[ ] 16. 创建 templates/ 完整目录树及 base.html、layouts/app.html、layouts/auth.html 骨架
|
||||
[ ] 17. 创建 components/ 模板骨架(topbar, sidebar, pagination, toast, modal, empty-state)
|
||||
@@ -871,9 +886,10 @@ addopts = "--reuse-db --cov=apps --cov=core --cov-report=term-missing -n auto"
|
||||
[ ] 25. 创建 manage.py
|
||||
[ ] 26. 验证:python manage.py check --deploy 无致命错误
|
||||
[ ] 27. 验证:项目目录树与第二节规范 100% 匹配
|
||||
[ ] 28. 验证:API_CONTRACT 核对清单 7 项全部完成(含 openapi.json 生成与 schemathesis 骨架)
|
||||
```
|
||||
---
|
||||
## 十六、关键注意事项
|
||||
## 十七、关键注意事项
|
||||
1. **django-tenants `apps/tenant/models.py`** 必须定义 `Tenant`(继承 `TenantMixin`)和 `Domain`(继承 `DomainMixin`),且 `Tenant` 的 `auto_create_schema = True`。
|
||||
2. **`shared/` App** 的 `apps.py` 中 `name = "shared"`,用于公共 Schema 的跨租户共享数据(如 PermissionDef 等)。
|
||||
3. **所有 App 的 `apps.py`** 必须包含正确的 `name`(含包路径,如 `apps.property`)和 `verbose_name`(中文)。
|
||||
@@ -883,4 +899,4 @@ addopts = "--reuse-db --cov=apps --cov=core --cov-report=term-missing -n auto"
|
||||
7. **模板中所有异步 HTMX 请求**在骨架阶段只需占位,但必须包含正确的 `hx-` 属性结构,不可省略 `hx-target` 和 `hx-swap`。
|
||||
8. **Toast 系统**:前端监听 `htmx:afterRequest` 事件,检查响应头 `HX-Trigger` 中的 `fonrey:toast`,动态插入 Toast DOM,4 秒自动消失。
|
||||
9. **小屏拦截**:`layouts/app.html` 中内嵌 JS,`window.innerWidth < 1280` 时显示全屏遮罩,文案:"Fonrey 当前仅支持桌面端(≥1280px),请在电脑上访问"。
|
||||
10. **所有密码、密钥、Tenant ID** 禁止出现在任何 Python 文件中,统一从 `python-decouple` 的 `env()` 读取。
|
||||
10. **所有密码、密钥、Tenant ID** 禁止出现在任何 Python 文件中,统一从 `python-decouple` 的 `env()` 读取。
|
||||
Reference in New Issue
Block a user