Merge branch 'main' of https://git.apinb.com/heqiapp/platforms
This commit is contained in:
@@ -12,18 +12,33 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// gasAdminRoleCode 是气站管理后台当前唯一允许登录的角色编码。
|
||||||
|
const gasAdminRoleCode = "admin"
|
||||||
|
|
||||||
|
// newGasAccount 构造平台创建的气站账户,并固定为气站管理员角色。
|
||||||
|
func newGasAccount(gasBasicID uint64, request accountRequest, passwordHash string) models.GasAccount {
|
||||||
|
return models.GasAccount{
|
||||||
|
Entity: common.NewEntity(common.StatusEnable),
|
||||||
|
GasBasicID: gasBasicID,
|
||||||
|
Username: request.Username,
|
||||||
|
DisplayName: request.DisplayName,
|
||||||
|
PasswordHash: passwordHash,
|
||||||
|
RoleCode: gasAdminRoleCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type accountRequest struct {
|
type accountRequest struct {
|
||||||
Username string `json:"username" binding:"required,max=64"`
|
Username string `json:"username" binding:"required,max=64"`
|
||||||
Password string `json:"password" binding:"required"`
|
Password string `json:"password" binding:"required"`
|
||||||
DisplayName string `json:"display_name" binding:"max=64"`
|
DisplayName string `json:"display_name" binding:"max=64"`
|
||||||
RoleCode string `json:"role_code" binding:"max=64"`
|
RoleCode string `json:"role_code" binding:"max=64"` // 兼容旧客户端,服务端固定使用 admin。
|
||||||
GasBasicIdentity string `json:"gas_basic_identity"`
|
GasBasicIdentity string `json:"gas_basic_identity"`
|
||||||
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
|
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type accountUpdateRequest struct {
|
type accountUpdateRequest struct {
|
||||||
DisplayName string `json:"display_name" binding:"max=64"`
|
DisplayName string `json:"display_name" binding:"max=64"`
|
||||||
RoleCode string `json:"role_code" binding:"max=64"`
|
RoleCode string `json:"role_code" binding:"max=64"` // 兼容旧客户端,更新时不采纳。
|
||||||
GasBasicIdentity string `json:"gas_basic_identity"`
|
GasBasicIdentity string `json:"gas_basic_identity"`
|
||||||
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
|
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
|
||||||
}
|
}
|
||||||
@@ -63,7 +78,7 @@ func CreateGasAccount(ctx *gin.Context) {
|
|||||||
infra.Response.Error(ctx, err)
|
infra.Response.Error(ctx, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
account := models.GasAccount{Entity: common.NewEntity(common.StatusEnable), GasBasicID: gasBasicID, Username: request.Username, DisplayName: request.DisplayName, PasswordHash: hash, RoleCode: request.RoleCode}
|
account := newGasAccount(gasBasicID, request, hash)
|
||||||
if err := impl.DBService.Create(&account).Error; err != nil {
|
if err := impl.DBService.Create(&account).Error; err != nil {
|
||||||
infra.Response.Error(ctx, err)
|
infra.Response.Error(ctx, err)
|
||||||
return
|
return
|
||||||
@@ -82,7 +97,7 @@ func UpdateGasAccount(ctx *gin.Context) {
|
|||||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
common.UpdateAllowedByIdentity(ctx, &models.GasAccount{}, gin.H{"gas_basic_id": gasBasicID, "display_name": request.DisplayName, "role_code": request.RoleCode}, []string{"gas_basic_id", "display_name", "role_code"})
|
common.UpdateAllowedByIdentity(ctx, &models.GasAccount{}, gin.H{"gas_basic_id": gasBasicID, "display_name": request.DisplayName}, []string{"gas_basic_id", "display_name"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitOwnerIdentities(value string) []string {
|
func splitOwnerIdentities(value string) []string {
|
||||||
|
|||||||
19
backend/api/internal/logic/platform/gas/account_test.go
Normal file
19
backend/api/internal/logic/platform/gas/account_test.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package gas
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// TestNewGasAccountUsesAdminRole 验证旧客户端提交的角色不会改变气站管理员权限。
|
||||||
|
func TestNewGasAccountUsesAdminRole(t *testing.T) {
|
||||||
|
account := newGasAccount(
|
||||||
|
9,
|
||||||
|
accountRequest{
|
||||||
|
Username: "gas-admin",
|
||||||
|
DisplayName: "气站管理员",
|
||||||
|
RoleCode: "legacy-role",
|
||||||
|
},
|
||||||
|
"password-hash",
|
||||||
|
)
|
||||||
|
if account.RoleCode != gasAdminRoleCode {
|
||||||
|
t.Fatalf("气站管理员角色编码必须为 %q,实际为 %q", gasAdminRoleCode, account.RoleCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -114,9 +114,9 @@
|
|||||||
|
|
||||||
新建和编辑页使用全宽信息卡,内部表单内容最大宽度为 `1440px`,并按内容容器宽度在双列与单列之间切换。页面内容从顶部自然排列,只保留面包屑和右侧返回按钮,不重复展示“新建/编辑资源名称”及操作说明;字段和底部操作按钮保持统一基线。账户头像摘要和详情页展示结构不随该表单布局调整而改变。
|
新建和编辑页使用全宽信息卡,内部表单内容最大宽度为 `1440px`,并按内容容器宽度在双列与单列之间切换。页面内容从顶部自然排列,只保留面包屑和右侧返回按钮,不重复展示“新建/编辑资源名称”及操作说明;字段和底部操作按钮保持统一基线。账户头像摘要和详情页展示结构不随该表单布局调整而改变。
|
||||||
|
|
||||||
标准详情页同样只保留面包屑、编辑和返回操作,不重复展示“详情资源名称”及说明文字。详情卡片按内容自然高度从顶部排列,统一使用 `12px` 间距和紧凑内边距;基本信息在大屏、中屏和小屏分别采用三列、两列和单列。未开通钱包使用单行提示,气站启停继续保留独立状态管理卡片,不改变原有接口和权限校验。
|
标准详情页同样只保留面包屑、编辑和返回操作,不重复展示“详情资源名称”及说明文字。详情卡片按内容自然高度从顶部排列,统一使用 `12px` 间距和紧凑内边距;基本信息在大屏、中屏和小屏分别采用三列、两列和单列。字段标签按文字自然宽度与值保持 `12px` 间距,长标签禁止拆字换行,不使用固定标签列制造多余空白。未开通钱包使用单行提示,气站启停继续保留独立状态管理卡片,不改变原有接口和权限校验。
|
||||||
|
|
||||||
新建与编辑使用不同字段规则。唯一标识、用户名、创建编码、不可变归属等创建后锁定字段只读展示且禁止修改;后端更新协议要求原归属标识时,页面只会原样回传该字段。合同状态、检修结果、系统角色和平台权限等限制同时由页面和后端校验。编辑页离开前检测未保存内容,详情页的聚合子表、钱包摘要、气站状态开关及资源专属业务动作继续保留。
|
新建与编辑使用不同字段规则。唯一标识、用户名、创建编码、不可变归属等创建后锁定字段以灰色禁用控件展示且禁止修改,不重复显示顶部说明条、字段旁提示或禁用占位文字;后端更新协议要求原归属标识时,页面只会原样回传该字段。合同状态、检修结果、系统角色和平台权限等限制同时由页面和后端校验。编辑页离开前检测未保存内容,详情页的聚合子表、钱包摘要、气站状态开关及资源专属业务动作继续保留。
|
||||||
|
|
||||||
### 6.1 机构管理
|
### 6.1 机构管理
|
||||||
|
|
||||||
@@ -129,6 +129,10 @@
|
|||||||
|
|
||||||
气站只能由平台总后台新增,创建后直接进入启用状态,无需审核。已启用或已停用气站可继续切换启停状态。
|
气站只能由平台总后台新增,创建后直接进入启用状态,无需审核。已启用或已停用气站可继续切换启停状态。
|
||||||
|
|
||||||
|
气站账户与配送点账户当前均只有管理员角色。新建时服务端分别固定写入 `admin`,编辑时不允许变更角色;旧客户端继续提交 `role_code` 时服务端兼容接收但不采纳。列表、详情、新建和编辑页面统一显示“角色”,并将 `admin` 显示为“气站管理员”或“配送点管理员”。历史未知编码显示为“未知角色(原编码)”,不得自动改成管理员。
|
||||||
|
|
||||||
|
气站账户、配送点账户和生产商账户没有头像字段与受控头像接口。它们的详情和编辑页使用紧凑文字账户摘要,新建页直接显示基本信息表单,不展示无法上传或保存的默认头像。工作人员、用户和平台账户继续使用真实头像上传与受控读取链路。
|
||||||
|
|
||||||
### 6.2 工作人员管理
|
### 6.2 工作人员管理
|
||||||
|
|
||||||
| 资源 | 路径 | 模式 | 已实现能力 |
|
| 资源 | 路径 | 模式 | 已实现能力 |
|
||||||
@@ -165,6 +169,8 @@
|
|||||||
|
|
||||||
智能气阀必须关联生产商和类型,可选关联库房、气站、配送点或用户。生产商登录密码仅保存哈希且不通过资源接口返回;独立生产端登录路由待生产管理系统落地时接入。归属组合由后端校验,不允许形成非法多重归属。生命周期支持待处理、在库、运输中、使用中、维修中和已报废;状态变化使用 `/product_info/:identity/lifecycle`,不能通过通用编辑直接改写。归属变化自动追加归属记录。
|
智能气阀必须关联生产商和类型,可选关联库房、气站、配送点或用户。生产商登录密码仅保存哈希且不通过资源接口返回;独立生产端登录路由待生产管理系统落地时接入。归属组合由后端校验,不允许形成非法多重归属。生命周期支持待处理、在库、运输中、使用中、维修中和已报废;状态变化使用 `/product_info/:identity/lifecycle`,不能通过通用编辑直接改写。归属变化自动追加归属记录。
|
||||||
|
|
||||||
|
生产商账户当前没有完整角色权限字典。本阶段仅移除无效头像占位,不改变其 `role_code` 写入规则;待生产管理系统明确角色模型后再统一中文角色名称和权限约束。
|
||||||
|
|
||||||
### 6.5 配送合同
|
### 6.5 配送合同
|
||||||
|
|
||||||
| 资源 | 路径 | 模式 | 已实现能力 |
|
| 资源 | 路径 | 模式 | 已实现能力 |
|
||||||
|
|||||||
@@ -40,6 +40,9 @@
|
|||||||
|
|
||||||
- 气站账号由平台总后台创建;气站端不允许自助创建管理员。
|
- 气站账号由平台总后台创建;气站端不允许自助创建管理员。
|
||||||
- 仅启用状态、关联启用气站且 `role_code = admin` 的 `gas_account` 可以登录。
|
- 仅启用状态、关联启用气站且 `role_code = admin` 的 `gas_account` 可以登录。
|
||||||
|
- 平台创建气站账户时固定保存 `role_code = admin`,更新账户时不允许修改角色;旧客户端携带该字段时兼容接收但忽略。
|
||||||
|
- 管理页面将 `admin` 显示为“气站管理员”;历史未知编码显示为“未知角色(原编码)”,不自动迁移或提升权限。
|
||||||
|
- 气站账户没有头像字段和受控头像接口,账户资料不得展示可操作或可持久化的头像入口。
|
||||||
- JWT 客户端固定为 `gas_admin`,与平台总后台会话隔离。
|
- JWT 客户端固定为 `gas_admin`,与平台总后台会话隔离。
|
||||||
- 登录后由后端 `gas_menu` 接口按 `role_code` 返回静态菜单,前端据此渲染。
|
- 登录后由后端 `gas_menu` 接口按 `role_code` 返回静态菜单,前端据此渲染。
|
||||||
- 修改密码必须校验当前密码;新密码仅校验长度不少于 6 位。
|
- 修改密码必须校验当前密码;新密码仅校验长度不少于 6 位。
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ Global:
|
|||||||
|
|
||||||
- 配送点管理员账号由平台或所属气站创建。
|
- 配送点管理员账号由平台或所属气站创建。
|
||||||
- 仅启用、关联启用配送点且 `role_code = admin` 的 `delivery_account` 可以登录。
|
- 仅启用、关联启用配送点且 `role_code = admin` 的 `delivery_account` 可以登录。
|
||||||
|
- 平台或所属气站创建配送点账户时固定保存 `role_code = admin`,更新账户时不允许修改角色。
|
||||||
|
- 三套管理后台统一使用“配送点账户”和“角色”,并将 `admin` 显示为“配送点管理员”;历史未知编码显示为“未知角色(原编码)”。
|
||||||
|
- 配送点账户没有头像字段和受控头像接口,不展示无法上传或保存的默认头像。
|
||||||
- JWT 客户端固定为 `delivery_admin`,与平台和气站管理端会话隔离。
|
- JWT 客户端固定为 `delivery_admin`,与平台和气站管理端会话隔离。
|
||||||
- 菜单由后端 `delivery_menu` 接口按 `role_code` 返回,前端登录后据此渲染。
|
- 菜单由后端 `delivery_menu` 接口按 `role_code` 返回,前端登录后据此渲染。
|
||||||
- 修改本人密码必须校验当前密码;新密码只要求不少于 6 位。
|
- 修改本人密码必须校验当前密码;新密码只要求不少于 6 位。
|
||||||
|
|||||||
37
docs/操作日志_标准资源编辑页提示精简_20260811.md
Normal file
37
docs/操作日志_标准资源编辑页提示精简_20260811.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# 标准资源编辑页提示精简操作日志
|
||||||
|
|
||||||
|
操作时间:2026-08-11 01:14:16
|
||||||
|
操作类型:修改
|
||||||
|
影响模块:平台总后台标准资源编辑页
|
||||||
|
|
||||||
|
## 操作前状态
|
||||||
|
|
||||||
|
- 编辑页顶部显示“灰色字段为创建后不可修改的信息,不会提交到更新接口”的蓝色说明条。
|
||||||
|
- 每个禁用字段旁重复显示“创建后不可修改”,文本会挤压输入框并在窄空间换行。
|
||||||
|
- 禁用文本输入框在值为空时仍显示“创建后不可修改”占位文字。
|
||||||
|
|
||||||
|
## 具体操作
|
||||||
|
|
||||||
|
- 修改 `frontend/platform_admin/src/views/resource/ResourceRecordPage.vue`,删除编辑页顶部重复说明条。
|
||||||
|
- 修改 `frontend/platform_admin/src/views/resource/ResourceFieldForm.vue`,删除禁用字段旁提示和禁用占位文字。
|
||||||
|
- 修改 `frontend/platform_admin/src/views/resource/ResourceRecordPage.less`,清理说明条已不再使用的样式选择器。
|
||||||
|
- 保留灰色禁用样式、只读字段展示、允许更新字段计算和更新请求过滤逻辑。
|
||||||
|
|
||||||
|
## 操作后状态
|
||||||
|
|
||||||
|
- 所有标准资源编辑页直接从字段区域开始,不保留说明条空位。
|
||||||
|
- 禁用字段和值继续显示,输入框宽度与同列其他字段一致。
|
||||||
|
- 页面不再重复解释不可修改状态,更新安全边界保持不变。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- `npm.cmd run type:check`:通过。
|
||||||
|
- `npm.cmd run build`:通过,Vite 生产构建完成。
|
||||||
|
- `npm.cmd run resource-pages:check`:通过,详情 46 类、新建 25 类、编辑 23 类。
|
||||||
|
- `npm.cmd run contract:check`:通过,48 个资源契约一致。
|
||||||
|
- 浏览器回归:气站编辑页顶部说明条、字段旁提示和禁用占位文字均已消失;禁用输入框与普通输入框宽度一致,控制台无警告或错误;未执行保存操作。
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
|
||||||
|
- 本次只删除视觉提示,不改变字段权限、表单值、校验、请求负载或后端接口。
|
||||||
|
- 新建页、详情页、业务动作弹窗、气站后台 5175 和配送点后台 5176 不受影响。
|
||||||
39
docs/操作日志_标准资源详情字段布局优化_20260811.md
Normal file
39
docs/操作日志_标准资源详情字段布局优化_20260811.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 标准资源详情字段布局优化操作日志
|
||||||
|
|
||||||
|
操作时间:2026-08-11 01:21:54
|
||||||
|
操作类型:修改
|
||||||
|
影响模块:平台总后台标准资源详情页
|
||||||
|
|
||||||
|
## 操作前状态
|
||||||
|
|
||||||
|
- 详情字段使用固定 `120px` 标签列,短标签和值之间出现明显空白。
|
||||||
|
- “统一社会信用代码”等长中文标签空间不足,会拆字换行并增加行高。
|
||||||
|
- 固定标签列在不同字段长度下无法同时兼顾紧凑性和可读性。
|
||||||
|
|
||||||
|
## 具体操作
|
||||||
|
|
||||||
|
- 修改 `frontend/platform_admin/src/views/resource/ResourceDetailContent.vue`:
|
||||||
|
- 标签列改为按文字自然宽度计算。
|
||||||
|
- 标签和值之间统一使用 `12px` 间距。
|
||||||
|
- 标签禁止换行,值区域继续允许收缩和自动换行。
|
||||||
|
- 小屏不再恢复固定 `100px` 标签列。
|
||||||
|
- 更新平台需求和标准资源全页管理项目文档。
|
||||||
|
|
||||||
|
## 操作后状态
|
||||||
|
|
||||||
|
- 短标签后的多余空白消失,字段值紧邻对应标签。
|
||||||
|
- 长中文标签保持单行,不再出现单字换行。
|
||||||
|
- 大屏三列、中屏两列、小屏单列及字段分隔线保持不变。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- `npm.cmd run type:check`:通过。
|
||||||
|
- `npm.cmd run build`:通过,Vite 生产构建完成。
|
||||||
|
- `npm.cmd run resource-pages:check`:通过,详情 46 类、新建 25 类、编辑 23 类。
|
||||||
|
- `npm.cmd run contract:check`:通过,48 个资源契约一致。
|
||||||
|
- 浏览器回归:“唯一标识”和“统一社会信用代码”标签与值的实际间距均为 `12px`,长标签保持单行;`700px` 小屏使用单列且无水平溢出,控制台无警告或错误。
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
|
||||||
|
- 本次仅调整详情字段 CSS,不修改字段顺序、格式化、唯一标识复制、接口或权限逻辑。
|
||||||
|
- 聚合子表、钱包、状态管理、编辑页、新建页及其他管理端保持原状。
|
||||||
57
docs/操作日志_账户角色中文展示与摘要统一_20260811.md
Normal file
57
docs/操作日志_账户角色中文展示与摘要统一_20260811.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# 操作日志:账户角色中文展示与摘要统一
|
||||||
|
|
||||||
|
操作时间:2026-08-11 01:47:12
|
||||||
|
|
||||||
|
操作类型:修改、扩展
|
||||||
|
|
||||||
|
影响模块:平台总后台、气站后台、配送点后台、平台账户接口
|
||||||
|
|
||||||
|
## 操作前状态
|
||||||
|
|
||||||
|
- 配送点账户曾实现“配送点管理员”中文展示,但标准资源独立页面改造后退回“角色编码 / admin”。
|
||||||
|
- 气站登录只允许 `admin`,平台创建和更新接口却仍接受任意角色编码,可能产生无法登录的账户。
|
||||||
|
- 气站、配送点和生产商账户没有头像字段或受控头像接口,平台页面仍展示默认头像占位。
|
||||||
|
- 平台账户编辑下拉使用角色名称,但列表和详情仍可能显示原始角色编码。
|
||||||
|
|
||||||
|
## 具体操作
|
||||||
|
|
||||||
|
1. 为气站和配送点账户增加资源级固定角色元数据,统一显示“角色”“气站管理员”“配送点管理员”。
|
||||||
|
2. 气站账户创建强制写入 `admin`,更新不再修改角色;保留旧请求字段用于兼容但不采纳。
|
||||||
|
3. 历史未知角色统一显示为“未知角色(原编码)”,不执行数据库迁移,不自动扩大权限。
|
||||||
|
4. 平台账户列表和详情加载动态角色名称,用角色名称替代裸编码。
|
||||||
|
5. 无真实头像能力的账户隐藏头像列并使用文字摘要;新建页不展示空的唯一标识和创建时间。具备头像接口的工作人员、用户和平台账户保持原功能。
|
||||||
|
6. 气站后台和配送点后台的账户管理弹层同步使用中文只读角色,并停止向账户更新接口提交角色字段。
|
||||||
|
|
||||||
|
## 代码变更
|
||||||
|
|
||||||
|
- `backend/api/internal/logic/platform/gas/account.go`:固定气站管理员角色并禁止更新角色。
|
||||||
|
- `backend/api/internal/logic/platform/gas/account_test.go`:验证旧角色字段不能改变管理员角色。
|
||||||
|
- `frontend/platform_admin/src/api/`:增加固定角色、未知角色和动态平台角色显示能力。
|
||||||
|
- `frontend/platform_admin/src/views/resource/`:按真实头像能力切换头像摘要或文字摘要。
|
||||||
|
- `frontend/gas_admin/src/`、`frontend/delivery_admin/src/`:统一账户角色中文名称和只读行为。
|
||||||
|
- `frontend/platform_admin/scripts/check-account-role-presentation.mjs`:静态检查三套后台的角色与摘要规则。
|
||||||
|
|
||||||
|
## 行为变化
|
||||||
|
|
||||||
|
| 场景 | 修改前 | 修改后 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 配送点账户角色 | “角色编码 / admin” | “角色 / 配送点管理员” |
|
||||||
|
| 气站账户角色 | 可输入任意编码 | 固定“气站管理员”,后端保存 `admin` |
|
||||||
|
| 历史未知角色 | 直接显示裸编码 | 显示“未知角色(原编码)”,不自动修改 |
|
||||||
|
| 无头像账户 | 显示不可操作默认头像 | 详情、编辑使用文字摘要;新建直接进入表单 |
|
||||||
|
| 平台账户角色 | 列表和详情可能显示编码 | 使用动态平台角色名称 |
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- 平台总后台、气站后台、配送点后台 TypeScript 类型检查和生产构建全部通过。
|
||||||
|
- 三套后台资源契约检查通过;标准资源页面检查通过:详情 46 类、新建 25 类、编辑 23 类。
|
||||||
|
- 账户角色静态检查通过,确认三套后台中文角色、未知角色和头像摘要规则均已覆盖。
|
||||||
|
- 后端 `go test ./...` 全量测试通过,包含新增气站管理员角色约束测试。
|
||||||
|
- 本地浏览器已验证应用可正常进入登录页;数据页视觉回归因当前测试会话未登录而未执行,未使用或提交本地默认密码。
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
|
||||||
|
- 不迁移历史非 `admin` 记录,避免静默赋予管理员权限;管理员需根据“未知角色”提示单独核查。
|
||||||
|
- 气站账户从本次变更起无法创建新角色,符合当前登录与菜单仅支持 `admin` 的既有约束。
|
||||||
|
- 生产商角色模型尚未明确,本次不修改其角色写入规则。
|
||||||
|
- 本次不新增气站、配送点或生产商头像字段,因此不产生数据库迁移和孤儿上传文件。
|
||||||
@@ -69,10 +69,10 @@ platforms/
|
|||||||
|
|
||||||
- 详情页顶部只保留“列表 / 详情”面包屑、编辑和返回操作,不重复展示“详情资源名称”及说明文字。
|
- 详情页顶部只保留“列表 / 详情”面包屑、编辑和返回操作,不重复展示“详情资源名称”及说明文字。
|
||||||
- 页面网格从顶部按内容自然排列,详情卡片统一使用 `12px` 间距、圆角、标题高度和内容内边距,不随剩余视口高度拉伸。
|
- 页面网格从顶部按内容自然排列,详情卡片统一使用 `12px` 间距、圆角、标题高度和内容内边距,不随剩余视口高度拉伸。
|
||||||
- 普通资源使用响应式三列信息区,金额、状态、日期、布尔和关系字段统一格式化。
|
- 普通资源使用响应式三列信息区;字段标签按文字自然宽度与值保持 `12px` 间距,长标签保持单行,避免短标签后留白或中文标签拆字换行;金额、状态、日期、布尔和关系字段统一格式化。
|
||||||
- 聚合详情中的数组继续以页签和表格展示,例如合同产品、修订记录和订单轨迹。
|
- 聚合详情中的数组继续以页签和表格展示,例如合同产品、修订记录和订单轨迹。
|
||||||
- 气站详情保留独立的紧凑启停区域;钱包归属资源保留钱包摘要和钱包详情入口,未开通钱包时显示单行紧凑提示。
|
- 气站详情保留独立的紧凑启停区域;钱包归属资源保留钱包摘要和钱包详情入口,未开通钱包时显示单行紧凑提示。
|
||||||
- 工作人员、用户和平台账户保留头像、用户名、唯一标识、创建时间的账户摘要。
|
- 工作人员、用户和平台账户保留头像、用户名、唯一标识、创建时间的账户摘要;气站、配送点和生产商账户仅展示紧凑文字摘要,不显示无数据能力的装饰头像。
|
||||||
- 资源专属流程动作继续使用短模态框,危险动作显示风险提示,平台角色菜单分配保留专属适配。
|
- 资源专属流程动作继续使用短模态框,危险动作显示风险提示,平台角色菜单分配保留专属适配。
|
||||||
|
|
||||||
### 4.3 新建与编辑
|
### 4.3 新建与编辑
|
||||||
@@ -80,8 +80,8 @@ platforms/
|
|||||||
- 新建和编辑卡片铺满主内容区,卡片内部使用最大 `1440px` 的内容容器控制输入框行长;标题、字段和操作按钮保持同一左基线。
|
- 新建和编辑卡片铺满主内容区,卡片内部使用最大 `1440px` 的内容容器控制输入框行长;标题、字段和操作按钮保持同一左基线。
|
||||||
- 表单根据容器实际宽度响应:内容区达到约 `900px` 时使用双列,否则切换为单列;地址、备注、条款、参数和正文等长字段独占整行。
|
- 表单根据容器实际宽度响应:内容区达到约 `900px` 时使用双列,否则切换为单列;地址、备注、条款、参数和正文等长字段独占整行。
|
||||||
- 页面网格从顶部自然排列;新建、编辑页只保留“列表 / 当前操作”面包屑和右侧返回按钮,不重复展示操作大标题及说明文字;保存和取消按钮位于表单末尾,不吸附浏览器底部。
|
- 页面网格从顶部自然排列;新建、编辑页只保留“列表 / 当前操作”面包屑和右侧返回按钮,不重复展示操作大标题及说明文字;保存和取消按钮位于表单末尾,不吸附浏览器底部。
|
||||||
- 账户类资源继续保留头像和身份摘要的居中结构,详情页布局不受表单优化影响。
|
- 具备受控头像接口的账户继续保留头像和身份摘要;没有头像能力的账户在新建页直接进入表单,在编辑页使用紧凑文字摘要。
|
||||||
- 编辑页展示创建后不可修改字段,但禁用字段不会进入更新请求。
|
- 编辑页继续以禁用控件展示创建后不可修改字段,不额外显示说明条、字段旁提示或禁用占位文字;禁用字段不会进入更新请求。
|
||||||
- 必填、密码最短 6 位、关系选项和金额转换复用统一校验与负载构建逻辑。
|
- 必填、密码最短 6 位、关系选项和金额转换复用统一校验与负载构建逻辑。
|
||||||
- 页面检测表单和头像变更;返回、取消或浏览器路由离开时均提示是否放弃未保存内容。
|
- 页面检测表单和头像变更;返回、取消或浏览器路由离开时均提示是否放弃未保存内容。
|
||||||
- 机构“账户管理”不再打开二级模态框,而是进入带机构过滤条件的隐藏账户列表及其独立 CRUD 页面。
|
- 机构“账户管理”不再打开二级模态框,而是进入带机构过滤条件的隐藏账户列表及其独立 CRUD 页面。
|
||||||
@@ -91,6 +91,8 @@ platforms/
|
|||||||
- 详情始终调用现有受保护资源接口,不使用列表缓存绕过脱敏和对象权限。
|
- 详情始终调用现有受保护资源接口,不使用列表缓存绕过脱敏和对象权限。
|
||||||
- 头像继续通过专用上传与受控读取接口;平台账户补充同等头像读取能力。
|
- 头像继续通过专用上传与受控读取接口;平台账户补充同等头像读取能力。
|
||||||
- 平台账户普通编辑没有提交头像时保持原值,避免更新其他资料时意外清空头像。
|
- 平台账户普通编辑没有提交头像时保持原值,避免更新其他资料时意外清空头像。
|
||||||
|
- 气站和配送点账户的 `admin` 分别显示为“气站管理员”和“配送点管理员”,表单只读;历史未知编码保留并显示明确提示,不做自动权限迁移。
|
||||||
|
- 平台账户列表和详情使用动态平台角色名称,不直接暴露角色编码;无法匹配时显示“未知角色(原编码)”。
|
||||||
- `return_to` 拒绝协议相对地址和站外地址。
|
- `return_to` 拒绝协议相对地址和站外地址。
|
||||||
- 附件查看未接入:现有通用响应会剥离敏感 URI,待受控下载接口完成后再扩展。
|
- 附件查看未接入:现有通用响应会剥离敏感 URI,待受控下载接口完成后再扩展。
|
||||||
|
|
||||||
@@ -112,6 +114,7 @@ platforms/
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
npm.cmd run resource-pages:check
|
npm.cmd run resource-pages:check
|
||||||
|
npm.cmd run account-roles:check
|
||||||
npm.cmd run contract:check
|
npm.cmd run contract:check
|
||||||
npm.cmd run build
|
npm.cmd run build
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export type PlatformMenu = { identity: string; parent_identity?: string; group_c
|
|||||||
export const platformApi = {
|
export const platformApi = {
|
||||||
overview: () => request<DashboardOverview>('/dashboard/overview'),
|
overview: () => request<DashboardOverview>('/dashboard/overview'),
|
||||||
listAccount: () => request<{ total: number; list: Record<string, unknown>[] }>('/platform_account'),
|
listAccount: () => request<{ total: number; list: Record<string, unknown>[] }>('/platform_account'),
|
||||||
listRole: () => resourceApi.list<PlatformRole>('/platform_role'),
|
listRole: () => resourceApi.list<PlatformRole>('/platform_role', 1, 100),
|
||||||
createRole: (data: Record<string, unknown>) => resourceApi.create<PlatformRole>('/platform_role', data),
|
createRole: (data: Record<string, unknown>) => resourceApi.create<PlatformRole>('/platform_role', data),
|
||||||
listMenu: () => request<{ total: number; list: PlatformMenu[] }>('/delivery_menu'),
|
listMenu: () => request<{ total: number; list: PlatformMenu[] }>('/delivery_menu'),
|
||||||
listRoleMenuIdentities: (identity: string) =>
|
listRoleMenuIdentities: (identity: string) =>
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export type ResourceField = {
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
relation?: string;
|
relation?: string;
|
||||||
options?: Array<{ label: string; value: string | number }>;
|
options?: Array<{ label: string; value: string | number }>;
|
||||||
|
defaultValue?: string | number | boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
unknownValueLabel?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DetailAction = {
|
export type DetailAction = {
|
||||||
@@ -63,7 +66,7 @@ const fieldLabels: Record<string, string> = {
|
|||||||
username: '用户名',
|
username: '用户名',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
display_name: '显示名称',
|
display_name: '显示名称',
|
||||||
role_code: '角色编码',
|
role_code: '角色',
|
||||||
platform_role_code: '平台角色',
|
platform_role_code: '平台角色',
|
||||||
credit_code: '统一社会信用代码',
|
credit_code: '统一社会信用代码',
|
||||||
principal: '负责人',
|
principal: '负责人',
|
||||||
@@ -272,6 +275,18 @@ function relation(key: string, resource: string, required = false): ResourceFiel
|
|||||||
return f(key, { type: 'identity', relation: resource, required });
|
return f(key, { type: 'identity', relation: resource, required });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 创建固定管理员角色字段,确保页面仅展示后端支持的角色。 */
|
||||||
|
function fixedAdminRole(label: string): ResourceField {
|
||||||
|
return f('role_code', {
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: [{ label, value: 'admin' }],
|
||||||
|
defaultValue: 'admin',
|
||||||
|
readonly: true,
|
||||||
|
unknownValueLabel: '未知角色',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function define(
|
function define(
|
||||||
name: string,
|
name: string,
|
||||||
title: string,
|
title: string,
|
||||||
@@ -310,9 +325,9 @@ const reason = [f('reason', { required: true })];
|
|||||||
|
|
||||||
const platformResources: ResourceUiDefinition[] = [
|
const platformResources: ResourceUiDefinition[] = [
|
||||||
{ ...define('gas_basic', '气站管理', 'writable', [f('code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('address'), f('longitude'), f('latitude')]), accountManagement: { resource: '/gas_account', relationKey: 'gas_basic_identity', title: '气站账户' }, walletOwnerType: 'gas' },
|
{ ...define('gas_basic', '气站管理', 'writable', [f('code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('address'), f('longitude'), f('latitude')]), accountManagement: { resource: '/gas_account', relationKey: 'gas_basic_identity', title: '气站账户' }, walletOwnerType: 'gas' },
|
||||||
define('gas_account', '气站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), relation('gas_basic_identity', '/gas_basic', true)]),
|
define('gas_account', '气站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('气站管理员'), relation('gas_basic_identity', '/gas_basic', true)]),
|
||||||
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), relation('gas_basic_identity', '/gas_basic'), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' }, walletOwnerType: 'delivery' },
|
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), relation('gas_basic_identity', '/gas_basic'), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' }, walletOwnerType: 'delivery' },
|
||||||
define('delivery_account', '配送站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), relation('delivery_basic_identity', '/delivery_basic', true)]),
|
define('delivery_account', '配送点账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('配送点管理员'), relation('delivery_basic_identity', '/delivery_basic', true)]),
|
||||||
{ ...define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })]), walletOwnerType: 'staff' },
|
{ ...define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })]), walletOwnerType: 'staff' },
|
||||||
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
||||||
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
|
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
|
||||||
@@ -409,7 +424,7 @@ const platformResources: ResourceUiDefinition[] = [
|
|||||||
|
|
||||||
const gasOverrides: ResourceUiDefinition[] = [
|
const gasOverrides: ResourceUiDefinition[] = [
|
||||||
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' } },
|
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' } },
|
||||||
define('delivery_account', '配送点账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name')], 'list', [
|
define('delivery_account', '配送点账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('配送点管理员')], 'list', [
|
||||||
{ name: '重置密码', resource: '/delivery_account/:identity/password', method: 'PUT', fields: [f('password', { required: true })] },
|
{ name: '重置密码', resource: '/delivery_account/:identity/password', method: 'PUT', fields: [f('password', { required: true })] },
|
||||||
]),
|
]),
|
||||||
define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), relation('delivery_basic_identity', '/delivery_basic'), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })], 'list', [
|
define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), relation('delivery_basic_identity', '/delivery_basic'), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })], 'list', [
|
||||||
|
|||||||
@@ -102,7 +102,9 @@
|
|||||||
<a-table-column title="唯一标识" :width="150"><template #cell="{ record }"><IdentityText :value="String(record.identity)" /></template></a-table-column>
|
<a-table-column title="唯一标识" :width="150"><template #cell="{ record }"><IdentityText :value="String(record.identity)" /></template></a-table-column>
|
||||||
<a-table-column title="用户名" data-index="username" :width="130" ellipsis tooltip />
|
<a-table-column title="用户名" data-index="username" :width="130" ellipsis tooltip />
|
||||||
<a-table-column title="显示名称" data-index="display_name" :width="130" ellipsis tooltip />
|
<a-table-column title="显示名称" data-index="display_name" :width="130" ellipsis tooltip />
|
||||||
<a-table-column title="角色编码" data-index="role_code" :width="100" ellipsis tooltip />
|
<a-table-column title="角色" :width="130" ellipsis tooltip>
|
||||||
|
<template #cell="{ record }">{{ accountRoleLabel(definition.accountManagement?.resource, record.role_code) }}</template>
|
||||||
|
</a-table-column>
|
||||||
<a-table-column title="状态" :width="90">
|
<a-table-column title="状态" :width="90">
|
||||||
<template #cell="{ record }">
|
<template #cell="{ record }">
|
||||||
<a-tag :color="Number(record.status) === 1 ? 'green' : 'red'">
|
<a-tag :color="Number(record.status) === 1 ? 'green' : 'red'">
|
||||||
@@ -151,8 +153,8 @@
|
|||||||
<a-form-item label="显示名称">
|
<a-form-item label="显示名称">
|
||||||
<a-input v-model="accountForm.display_name" />
|
<a-input v-model="accountForm.display_name" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="角色编码">
|
<a-form-item label="角色">
|
||||||
<a-input v-model="accountForm.role_code" />
|
<a-input :model-value="accountRoleLabel(definition.accountManagement?.resource, accountForm.role_code || 'admin')" disabled />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
@@ -166,8 +168,8 @@
|
|||||||
<a-date-picker v-else-if="field.type === 'datetime'" v-model="form[field.key]" show-time value-format="YYYY-MM-DDTHH:mm:ssZ" />
|
<a-date-picker v-else-if="field.type === 'datetime'" v-model="form[field.key]" show-time value-format="YYYY-MM-DDTHH:mm:ssZ" />
|
||||||
<a-textarea v-else-if="field.type === 'textarea'" v-model="form[field.key]" :auto-size="{ minRows: 3, maxRows: 8 }" />
|
<a-textarea v-else-if="field.type === 'textarea'" v-model="form[field.key]" :auto-size="{ minRows: 3, maxRows: 8 }" />
|
||||||
<a-input-password v-else-if="field.type === 'password'" v-model="form[field.key]" />
|
<a-input-password v-else-if="field.type === 'password'" v-model="form[field.key]" />
|
||||||
<a-select v-else-if="field.type === 'select'" v-model="form[field.key]" allow-clear>
|
<a-select v-else-if="field.type === 'select'" v-model="form[field.key]" :disabled="field.readonly" allow-clear>
|
||||||
<a-option v-for="option in field.options" :key="option.value" :value="option.value">{{ option.label }}</a-option>
|
<a-option v-for="option in selectFieldOptions(field)" :key="option.value" :value="option.value">{{ option.label }}</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
<a-select v-else-if="field.type === 'identity' || field.type === 'identity-list'" v-model="form[field.key]" :multiple="field.type === 'identity-list'" allow-clear allow-search :loading="relationLoading[field.relation ?? '']" @search="(value: string) => searchRelation(field.relation, value)">
|
<a-select v-else-if="field.type === 'identity' || field.type === 'identity-list'" v-model="form[field.key]" :multiple="field.type === 'identity-list'" allow-clear allow-search :loading="relationLoading[field.relation ?? '']" @search="(value: string) => searchRelation(field.relation, value)">
|
||||||
<a-option v-for="option in relationOptions[field.relation ?? ''] ?? []" :key="String(option.identity)" :value="String(option.identity)">
|
<a-option v-for="option in relationOptions[field.relation ?? ''] ?? []" :key="String(option.identity)" :value="String(option.identity)">
|
||||||
@@ -437,7 +439,7 @@ function resetForm(data?: Row) {
|
|||||||
const value = data?.[field.key];
|
const value = data?.[field.key];
|
||||||
form[field.key] =
|
form[field.key] =
|
||||||
value == null
|
value == null
|
||||||
? undefined
|
? field.defaultValue
|
||||||
: field.type === 'money'
|
: field.type === 'money'
|
||||||
? Number(value) / 100
|
? Number(value) / 100
|
||||||
: value;
|
: value;
|
||||||
@@ -655,7 +657,6 @@ async function saveAccount() {
|
|||||||
try {
|
try {
|
||||||
const payload: Record<string, unknown> = {
|
const payload: Record<string, unknown> = {
|
||||||
display_name: String(accountForm.display_name ?? '').trim(),
|
display_name: String(accountForm.display_name ?? '').trim(),
|
||||||
role_code: String(accountForm.role_code ?? '').trim(),
|
|
||||||
[management.relationKey]: ownerIdentity,
|
[management.relationKey]: ownerIdentity,
|
||||||
};
|
};
|
||||||
if (!accountEditingIdentity.value) {
|
if (!accountEditingIdentity.value) {
|
||||||
@@ -1010,6 +1011,8 @@ function displayFieldValue(field: ResourceField, row: Row) {
|
|||||||
if (field.options) {
|
if (field.options) {
|
||||||
const option = field.options.find((item) => item.value === value);
|
const option = field.options.find((item) => item.value === value);
|
||||||
if (option) return option.label;
|
if (option) return option.label;
|
||||||
|
if (field.unknownValueLabel && value != null && value !== '')
|
||||||
|
return `${field.unknownValueLabel}(${String(value)})`;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
(field.type === 'identity' || field.type === 'identity-list') &&
|
(field.type === 'identity' || field.type === 'identity-list') &&
|
||||||
@@ -1021,6 +1024,8 @@ function displayFieldValue(field: ResourceField, row: Row) {
|
|||||||
|
|
||||||
function displayValue(key: string, value: unknown) {
|
function displayValue(key: string, value: unknown) {
|
||||||
if (value == null || value === '') return '-';
|
if (value == null || value === '') return '-';
|
||||||
|
if (key === 'role_code')
|
||||||
|
return accountRoleLabel(props.definition.resource, value);
|
||||||
if (typeof value === 'boolean') return value ? '是' : '否';
|
if (typeof value === 'boolean') return value ? '是' : '否';
|
||||||
if (
|
if (
|
||||||
key === 'amount' ||
|
key === 'amount' ||
|
||||||
@@ -1045,6 +1050,40 @@ function displayValue(key: string, value: unknown) {
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将各管理端稳定角色编码转换为中文名称,未知编码保留审计线索。 */
|
||||||
|
function accountRoleLabel(resource: string | undefined, value: unknown) {
|
||||||
|
const code = String(value ?? '');
|
||||||
|
const labels: Record<string, Record<string, string>> = {
|
||||||
|
'/gas_account': { admin: '气站管理员' },
|
||||||
|
'/delivery_account': { admin: '配送点管理员' },
|
||||||
|
'/staff_account': {
|
||||||
|
installer: '安装人员',
|
||||||
|
delivery: '配送人员',
|
||||||
|
operations: '运维人员',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const label = labels[resource ?? '']?.[code];
|
||||||
|
return label ?? (code ? `未知角色(${code})` : '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 给只读角色下拉补充历史未知值,避免直接暴露裸编码。 */
|
||||||
|
function selectFieldOptions(field: ResourceField) {
|
||||||
|
const options = [...(field.options ?? [])];
|
||||||
|
const value = form[field.key];
|
||||||
|
if (
|
||||||
|
field.unknownValueLabel &&
|
||||||
|
value != null &&
|
||||||
|
value !== '' &&
|
||||||
|
!options.some((option) => String(option.value) === String(value))
|
||||||
|
) {
|
||||||
|
options.push({
|
||||||
|
label: `${field.unknownValueLabel}(${String(value)})`,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadRelation(resource: string, keyword = '') {
|
async function loadRelation(resource: string, keyword = '') {
|
||||||
relationLoading[resource] = true;
|
relationLoading[resource] = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export type PlatformMenu = { identity: string; parent_identity?: string; group_c
|
|||||||
export const platformApi = {
|
export const platformApi = {
|
||||||
overview: () => request<DashboardOverview>('/dashboard/overview'),
|
overview: () => request<DashboardOverview>('/dashboard/overview'),
|
||||||
listAccount: () => request<{ total: number; list: Record<string, unknown>[] }>('/platform_account'),
|
listAccount: () => request<{ total: number; list: Record<string, unknown>[] }>('/platform_account'),
|
||||||
listRole: () => resourceApi.list<PlatformRole>('/platform_role'),
|
listRole: () => resourceApi.list<PlatformRole>('/platform_role', 1, 100),
|
||||||
createRole: (data: Record<string, unknown>) => resourceApi.create<PlatformRole>('/platform_role', data),
|
createRole: (data: Record<string, unknown>) => resourceApi.create<PlatformRole>('/platform_role', data),
|
||||||
listMenu: () => request<{ total: number; list: PlatformMenu[] }>('/gas_menu'),
|
listMenu: () => request<{ total: number; list: PlatformMenu[] }>('/gas_menu'),
|
||||||
listRoleMenuIdentities: (identity: string) =>
|
listRoleMenuIdentities: (identity: string) =>
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export type ResourceField = {
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
relation?: string;
|
relation?: string;
|
||||||
options?: Array<{ label: string; value: string | number }>;
|
options?: Array<{ label: string; value: string | number }>;
|
||||||
|
defaultValue?: string | number | boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
unknownValueLabel?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DetailAction = {
|
export type DetailAction = {
|
||||||
@@ -63,7 +66,7 @@ const fieldLabels: Record<string, string> = {
|
|||||||
username: '用户名',
|
username: '用户名',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
display_name: '显示名称',
|
display_name: '显示名称',
|
||||||
role_code: '角色编码',
|
role_code: '角色',
|
||||||
platform_role_code: '平台角色',
|
platform_role_code: '平台角色',
|
||||||
credit_code: '统一社会信用代码',
|
credit_code: '统一社会信用代码',
|
||||||
principal: '负责人',
|
principal: '负责人',
|
||||||
@@ -272,6 +275,18 @@ function relation(key: string, resource: string, required = false): ResourceFiel
|
|||||||
return f(key, { type: 'identity', relation: resource, required });
|
return f(key, { type: 'identity', relation: resource, required });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 创建固定管理员角色字段,确保页面仅展示后端支持的角色。 */
|
||||||
|
function fixedAdminRole(label: string): ResourceField {
|
||||||
|
return f('role_code', {
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: [{ label, value: 'admin' }],
|
||||||
|
defaultValue: 'admin',
|
||||||
|
readonly: true,
|
||||||
|
unknownValueLabel: '未知角色',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function define(
|
function define(
|
||||||
name: string,
|
name: string,
|
||||||
title: string,
|
title: string,
|
||||||
@@ -310,9 +325,9 @@ const reason = [f('reason', { required: true })];
|
|||||||
|
|
||||||
const platformResources: ResourceUiDefinition[] = [
|
const platformResources: ResourceUiDefinition[] = [
|
||||||
{ ...define('gas_basic', '气站管理', 'writable', [f('code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('address'), f('longitude'), f('latitude')]), accountManagement: { resource: '/gas_account', relationKey: 'gas_basic_identity', title: '气站账户' }, walletOwnerType: 'gas' },
|
{ ...define('gas_basic', '气站管理', 'writable', [f('code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('address'), f('longitude'), f('latitude')]), accountManagement: { resource: '/gas_account', relationKey: 'gas_basic_identity', title: '气站账户' }, walletOwnerType: 'gas' },
|
||||||
define('gas_account', '气站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), relation('gas_basic_identity', '/gas_basic', true)]),
|
define('gas_account', '气站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('气站管理员'), relation('gas_basic_identity', '/gas_basic', true)]),
|
||||||
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), relation('gas_basic_identity', '/gas_basic'), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' }, walletOwnerType: 'delivery' },
|
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), relation('gas_basic_identity', '/gas_basic'), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' }, walletOwnerType: 'delivery' },
|
||||||
define('delivery_account', '配送站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), relation('delivery_basic_identity', '/delivery_basic', true)]),
|
define('delivery_account', '配送点账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('配送点管理员'), relation('delivery_basic_identity', '/delivery_basic', true)]),
|
||||||
{ ...define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })]), walletOwnerType: 'staff' },
|
{ ...define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })]), walletOwnerType: 'staff' },
|
||||||
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
||||||
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
|
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
|
||||||
@@ -409,7 +424,7 @@ const platformResources: ResourceUiDefinition[] = [
|
|||||||
|
|
||||||
const gasOverrides: ResourceUiDefinition[] = [
|
const gasOverrides: ResourceUiDefinition[] = [
|
||||||
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' } },
|
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' } },
|
||||||
define('delivery_account', '配送点账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name')], 'list', [
|
define('delivery_account', '配送点账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('配送点管理员')], 'list', [
|
||||||
{ name: '重置密码', resource: '/delivery_account/:identity/password', method: 'PUT', fields: [f('password', { required: true })] },
|
{ name: '重置密码', resource: '/delivery_account/:identity/password', method: 'PUT', fields: [f('password', { required: true })] },
|
||||||
]),
|
]),
|
||||||
define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), relation('delivery_basic_identity', '/delivery_basic'), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })], 'list', [
|
define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), relation('delivery_basic_identity', '/delivery_basic'), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })], 'list', [
|
||||||
|
|||||||
@@ -102,7 +102,9 @@
|
|||||||
<a-table-column title="唯一标识" :width="150"><template #cell="{ record }"><IdentityText :value="String(record.identity)" /></template></a-table-column>
|
<a-table-column title="唯一标识" :width="150"><template #cell="{ record }"><IdentityText :value="String(record.identity)" /></template></a-table-column>
|
||||||
<a-table-column title="用户名" data-index="username" :width="130" ellipsis tooltip />
|
<a-table-column title="用户名" data-index="username" :width="130" ellipsis tooltip />
|
||||||
<a-table-column title="显示名称" data-index="display_name" :width="130" ellipsis tooltip />
|
<a-table-column title="显示名称" data-index="display_name" :width="130" ellipsis tooltip />
|
||||||
<a-table-column title="角色编码" data-index="role_code" :width="100" ellipsis tooltip />
|
<a-table-column title="角色" :width="130" ellipsis tooltip>
|
||||||
|
<template #cell="{ record }">{{ accountRoleLabel(definition.accountManagement?.resource, record.role_code) }}</template>
|
||||||
|
</a-table-column>
|
||||||
<a-table-column title="状态" :width="90">
|
<a-table-column title="状态" :width="90">
|
||||||
<template #cell="{ record }">
|
<template #cell="{ record }">
|
||||||
<a-tag :color="Number(record.status) === 1 ? 'green' : 'red'">
|
<a-tag :color="Number(record.status) === 1 ? 'green' : 'red'">
|
||||||
@@ -151,8 +153,8 @@
|
|||||||
<a-form-item label="显示名称">
|
<a-form-item label="显示名称">
|
||||||
<a-input v-model="accountForm.display_name" />
|
<a-input v-model="accountForm.display_name" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="角色编码">
|
<a-form-item label="角色">
|
||||||
<a-input v-model="accountForm.role_code" />
|
<a-input :model-value="accountRoleLabel(definition.accountManagement?.resource, accountForm.role_code || 'admin')" disabled />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
@@ -166,8 +168,8 @@
|
|||||||
<a-date-picker v-else-if="field.type === 'datetime'" v-model="form[field.key]" show-time value-format="YYYY-MM-DDTHH:mm:ssZ" />
|
<a-date-picker v-else-if="field.type === 'datetime'" v-model="form[field.key]" show-time value-format="YYYY-MM-DDTHH:mm:ssZ" />
|
||||||
<a-textarea v-else-if="field.type === 'textarea'" v-model="form[field.key]" :auto-size="{ minRows: 3, maxRows: 8 }" />
|
<a-textarea v-else-if="field.type === 'textarea'" v-model="form[field.key]" :auto-size="{ minRows: 3, maxRows: 8 }" />
|
||||||
<a-input-password v-else-if="field.type === 'password'" v-model="form[field.key]" />
|
<a-input-password v-else-if="field.type === 'password'" v-model="form[field.key]" />
|
||||||
<a-select v-else-if="field.type === 'select'" v-model="form[field.key]" allow-clear>
|
<a-select v-else-if="field.type === 'select'" v-model="form[field.key]" :disabled="field.readonly" allow-clear>
|
||||||
<a-option v-for="option in field.options" :key="option.value" :value="option.value">{{ option.label }}</a-option>
|
<a-option v-for="option in selectFieldOptions(field)" :key="option.value" :value="option.value">{{ option.label }}</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
<a-select v-else-if="field.type === 'identity' || field.type === 'identity-list'" v-model="form[field.key]" :multiple="field.type === 'identity-list'" allow-clear allow-search :loading="relationLoading[field.relation ?? '']" @search="(value: string) => searchRelation(field.relation, value)">
|
<a-select v-else-if="field.type === 'identity' || field.type === 'identity-list'" v-model="form[field.key]" :multiple="field.type === 'identity-list'" allow-clear allow-search :loading="relationLoading[field.relation ?? '']" @search="(value: string) => searchRelation(field.relation, value)">
|
||||||
<a-option v-for="option in relationOptions[field.relation ?? ''] ?? []" :key="String(option.identity)" :value="String(option.identity)">
|
<a-option v-for="option in relationOptions[field.relation ?? ''] ?? []" :key="String(option.identity)" :value="String(option.identity)">
|
||||||
@@ -437,7 +439,7 @@ function resetForm(data?: Row) {
|
|||||||
const value = data?.[field.key];
|
const value = data?.[field.key];
|
||||||
form[field.key] =
|
form[field.key] =
|
||||||
value == null
|
value == null
|
||||||
? undefined
|
? field.defaultValue
|
||||||
: field.type === 'money'
|
: field.type === 'money'
|
||||||
? Number(value) / 100
|
? Number(value) / 100
|
||||||
: value;
|
: value;
|
||||||
@@ -655,7 +657,6 @@ async function saveAccount() {
|
|||||||
try {
|
try {
|
||||||
const payload: Record<string, unknown> = {
|
const payload: Record<string, unknown> = {
|
||||||
display_name: String(accountForm.display_name ?? '').trim(),
|
display_name: String(accountForm.display_name ?? '').trim(),
|
||||||
role_code: String(accountForm.role_code ?? '').trim(),
|
|
||||||
[management.relationKey]: ownerIdentity,
|
[management.relationKey]: ownerIdentity,
|
||||||
};
|
};
|
||||||
if (!accountEditingIdentity.value) {
|
if (!accountEditingIdentity.value) {
|
||||||
@@ -1010,6 +1011,8 @@ function displayFieldValue(field: ResourceField, row: Row) {
|
|||||||
if (field.options) {
|
if (field.options) {
|
||||||
const option = field.options.find((item) => item.value === value);
|
const option = field.options.find((item) => item.value === value);
|
||||||
if (option) return option.label;
|
if (option) return option.label;
|
||||||
|
if (field.unknownValueLabel && value != null && value !== '')
|
||||||
|
return `${field.unknownValueLabel}(${String(value)})`;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
(field.type === 'identity' || field.type === 'identity-list') &&
|
(field.type === 'identity' || field.type === 'identity-list') &&
|
||||||
@@ -1021,6 +1024,8 @@ function displayFieldValue(field: ResourceField, row: Row) {
|
|||||||
|
|
||||||
function displayValue(key: string, value: unknown) {
|
function displayValue(key: string, value: unknown) {
|
||||||
if (value == null || value === '') return '-';
|
if (value == null || value === '') return '-';
|
||||||
|
if (key === 'role_code')
|
||||||
|
return accountRoleLabel(props.definition.resource, value);
|
||||||
if (typeof value === 'boolean') return value ? '是' : '否';
|
if (typeof value === 'boolean') return value ? '是' : '否';
|
||||||
if (
|
if (
|
||||||
key === 'amount' ||
|
key === 'amount' ||
|
||||||
@@ -1045,6 +1050,40 @@ function displayValue(key: string, value: unknown) {
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将各管理端稳定角色编码转换为中文名称,未知编码保留审计线索。 */
|
||||||
|
function accountRoleLabel(resource: string | undefined, value: unknown) {
|
||||||
|
const code = String(value ?? '');
|
||||||
|
const labels: Record<string, Record<string, string>> = {
|
||||||
|
'/gas_account': { admin: '气站管理员' },
|
||||||
|
'/delivery_account': { admin: '配送点管理员' },
|
||||||
|
'/staff_account': {
|
||||||
|
installer: '安装人员',
|
||||||
|
delivery: '配送人员',
|
||||||
|
operations: '运维人员',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const label = labels[resource ?? '']?.[code];
|
||||||
|
return label ?? (code ? `未知角色(${code})` : '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 给只读角色下拉补充历史未知值,避免直接暴露裸编码。 */
|
||||||
|
function selectFieldOptions(field: ResourceField) {
|
||||||
|
const options = [...(field.options ?? [])];
|
||||||
|
const value = form[field.key];
|
||||||
|
if (
|
||||||
|
field.unknownValueLabel &&
|
||||||
|
value != null &&
|
||||||
|
value !== '' &&
|
||||||
|
!options.some((option) => String(option.value) === String(value))
|
||||||
|
) {
|
||||||
|
options.push({
|
||||||
|
label: `${field.unknownValueLabel}(${String(value)})`,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadRelation(resource: string, keyword = '') {
|
async function loadRelation(resource: string, keyword = '') {
|
||||||
relationLoading[resource] = true;
|
relationLoading[resource] = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"contract:sync": "node scripts/sync-backend-contract.mjs",
|
"contract:sync": "node scripts/sync-backend-contract.mjs",
|
||||||
"contract:check": "node scripts/check-backend-contract.mjs",
|
"contract:check": "node scripts/check-backend-contract.mjs",
|
||||||
"resource-pages:check": "node scripts/check-resource-pages.mjs",
|
"resource-pages:check": "node scripts/check-resource-pages.mjs",
|
||||||
|
"account-roles:check": "node scripts/check-account-role-presentation.mjs",
|
||||||
"audit:platform": "node scripts/check-backend-contract.mjs",
|
"audit:platform": "node scripts/check-backend-contract.mjs",
|
||||||
"lint": "biome lint .",
|
"lint": "biome lint .",
|
||||||
"lint:fix": "biome lint --write .",
|
"lint:fix": "biome lint --write .",
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* 功能:静态检查三套管理后台的账户角色中文展示与无效头像摘要规则。
|
||||||
|
* 版本:v1.0.0
|
||||||
|
*/
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { dirname, resolve } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const frontendRoot = resolve(appRoot, '..');
|
||||||
|
|
||||||
|
function source(path) {
|
||||||
|
return readFileSync(resolve(frontendRoot, path), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectIncludes(content, marker, message) {
|
||||||
|
if (!content.includes(marker)) throw new Error(`账户角色检查失败:${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const platformResources = source('platform_admin/src/api/resources.ts');
|
||||||
|
const platformPage = source(
|
||||||
|
'platform_admin/src/views/resource/ResourceRecordPage.vue',
|
||||||
|
);
|
||||||
|
const platformSummary = source(
|
||||||
|
'platform_admin/src/views/resource/ResourceAccountSummary.vue',
|
||||||
|
);
|
||||||
|
|
||||||
|
expectIncludes(
|
||||||
|
platformResources,
|
||||||
|
"fixedAdminRole('气站管理员')",
|
||||||
|
'平台总后台缺少气站管理员中文映射',
|
||||||
|
);
|
||||||
|
expectIncludes(
|
||||||
|
platformResources,
|
||||||
|
"fixedAdminRole('配送点管理员')",
|
||||||
|
'平台总后台缺少配送点管理员中文映射',
|
||||||
|
);
|
||||||
|
expectIncludes(
|
||||||
|
platformResources,
|
||||||
|
"define('delivery_account', '配送点账户'",
|
||||||
|
'配送点账户名称未统一',
|
||||||
|
);
|
||||||
|
expectIncludes(
|
||||||
|
platformPage,
|
||||||
|
"mode.value !== 'create' || hasAvatarField.value",
|
||||||
|
'无头像能力的新建账户页仍可能显示无效占位摘要',
|
||||||
|
);
|
||||||
|
expectIncludes(
|
||||||
|
platformSummary,
|
||||||
|
'v-if="avatarEnabled" class="avatar-column"',
|
||||||
|
'无头像能力的账户仍可能显示装饰头像',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const app of ['gas_admin', 'delivery_admin']) {
|
||||||
|
const resources = source(`${app}/src/api/resources.ts`);
|
||||||
|
const listPage = source(`${app}/src/views/shared/CrudListPage.vue`);
|
||||||
|
expectIncludes(
|
||||||
|
resources,
|
||||||
|
"fixedAdminRole('配送点管理员')",
|
||||||
|
`${app} 缺少配送点管理员中文映射`,
|
||||||
|
);
|
||||||
|
expectIncludes(
|
||||||
|
listPage,
|
||||||
|
"'/gas_account': { admin: '气站管理员' }",
|
||||||
|
`${app} 缺少气站管理员显示规则`,
|
||||||
|
);
|
||||||
|
expectIncludes(
|
||||||
|
listPage,
|
||||||
|
"'/delivery_account': { admin: '配送点管理员' }",
|
||||||
|
`${app} 缺少配送点管理员显示规则`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('账户角色检查通过:三套后台中文角色、未知角色和头像摘要规则均已覆盖。');
|
||||||
@@ -33,7 +33,7 @@ export type PlatformMenu = { identity: string; parent_identity?: string; group_c
|
|||||||
export const platformApi = {
|
export const platformApi = {
|
||||||
overview: () => request<DashboardOverview>('/dashboard/overview'),
|
overview: () => request<DashboardOverview>('/dashboard/overview'),
|
||||||
listAccount: () => request<{ total: number; list: Record<string, unknown>[] }>('/platform_account'),
|
listAccount: () => request<{ total: number; list: Record<string, unknown>[] }>('/platform_account'),
|
||||||
listRole: () => resourceApi.list<PlatformRole>('/platform_role'),
|
listRole: () => resourceApi.list<PlatformRole>('/platform_role', 1, 100),
|
||||||
createRole: (data: Record<string, unknown>) => resourceApi.create<PlatformRole>('/platform_role', data),
|
createRole: (data: Record<string, unknown>) => resourceApi.create<PlatformRole>('/platform_role', data),
|
||||||
listMenu: () => request<{ total: number; list: PlatformMenu[] }>('/platform_menu'),
|
listMenu: () => request<{ total: number; list: PlatformMenu[] }>('/platform_menu'),
|
||||||
listRoleMenuIdentities: (identity: string) =>
|
listRoleMenuIdentities: (identity: string) =>
|
||||||
|
|||||||
@@ -161,13 +161,21 @@ export function displayResourceField(
|
|||||||
field: ResourceField,
|
field: ResourceField,
|
||||||
row: ResourceRow,
|
row: ResourceRow,
|
||||||
relationOptions: Record<string, ResourceRow[]>,
|
relationOptions: Record<string, ResourceRow[]>,
|
||||||
|
fieldOptions: Record<string, Array<{ label: string; value: string | number }>> = {},
|
||||||
) {
|
) {
|
||||||
const value = row[field.key] ?? row[`${field.key}_masked`];
|
const value = row[field.key] ?? row[`${field.key}_masked`];
|
||||||
if (value == null || value === '') return field.emptyText ?? '-';
|
if (value == null || value === '') return field.emptyText ?? '-';
|
||||||
const option = field.options?.find(
|
const hasOptionSource =
|
||||||
|
Object.prototype.hasOwnProperty.call(fieldOptions, field.key) ||
|
||||||
|
Boolean(field.options);
|
||||||
|
const options = fieldOptions[field.key] ?? field.options;
|
||||||
|
const option = options?.find(
|
||||||
(item) => String(item.value) === String(value),
|
(item) => String(item.value) === String(value),
|
||||||
);
|
);
|
||||||
if (option) return option.label;
|
if (option) return option.label;
|
||||||
|
if (hasOptionSource && field.unknownValueLabel) {
|
||||||
|
return `${field.unknownValueLabel}(${String(value)})`;
|
||||||
|
}
|
||||||
if (field.type === 'identity' && typeof value === 'string') {
|
if (field.type === 'identity' && typeof value === 'string') {
|
||||||
return relationLabel(field, value, relationOptions);
|
return relationLabel(field, value, relationOptions);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,14 +33,13 @@ const pageRules: Record<string, ResourcePageRule> = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
gas_account: {
|
gas_account: {
|
||||||
editKeys: ['display_name', 'role_code', 'gas_basic_identity'],
|
editKeys: ['display_name', 'gas_basic_identity'],
|
||||||
accountSummary: true,
|
accountSummary: true,
|
||||||
},
|
},
|
||||||
delivery_basic: {
|
delivery_basic: {
|
||||||
editKeys: ['name', 'gas_basic_identity', 'principal', 'address'],
|
editKeys: ['name', 'gas_basic_identity', 'principal', 'address'],
|
||||||
},
|
},
|
||||||
delivery_account: {
|
delivery_account: {
|
||||||
createHiddenKeys: ['role_code'],
|
|
||||||
editKeys: ['display_name', 'delivery_basic_identity'],
|
editKeys: ['display_name', 'delivery_basic_identity'],
|
||||||
accountSummary: true,
|
accountSummary: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function resetResourceRecordForm(
|
|||||||
const value = row[field.key];
|
const value = row[field.key];
|
||||||
form[field.key] =
|
form[field.key] =
|
||||||
value == null
|
value == null
|
||||||
? undefined
|
? field.defaultValue
|
||||||
: field.type === 'money'
|
: field.type === 'money'
|
||||||
? Number(value) / 100
|
? Number(value) / 100
|
||||||
: value;
|
: value;
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ export type ResourceField = {
|
|||||||
emptyText?: string;
|
emptyText?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
options?: Array<{ label: string; value: string | number }>;
|
options?: Array<{ label: string; value: string | number }>;
|
||||||
|
defaultValue?: string | number | boolean;
|
||||||
|
readonlyOnCreate?: boolean;
|
||||||
|
unknownValueLabel?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DetailAction = {
|
export type DetailAction = {
|
||||||
@@ -68,7 +71,7 @@ const fieldLabels: Record<string, string> = {
|
|||||||
username: '用户名',
|
username: '用户名',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
display_name: '显示名称',
|
display_name: '显示名称',
|
||||||
role_code: '角色编码',
|
role_code: '角色',
|
||||||
platform_role_code: '平台角色',
|
platform_role_code: '平台角色',
|
||||||
credit_code: '统一社会信用代码',
|
credit_code: '统一社会信用代码',
|
||||||
principal: '负责人',
|
principal: '负责人',
|
||||||
@@ -281,6 +284,20 @@ function relation(key: string, resource: string, required = false): ResourceFiel
|
|||||||
return f(key, { type: 'identity', relation: resource, required });
|
return f(key, { type: 'identity', relation: resource, required });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 创建固定管理员角色字段,页面展示中文名称,接口仍使用稳定编码。 */
|
||||||
|
function fixedAdminRole(label: string): ResourceField {
|
||||||
|
return f('role_code', {
|
||||||
|
label: '角色',
|
||||||
|
listLabel: '角色',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: [{ label, value: 'admin' }],
|
||||||
|
defaultValue: 'admin',
|
||||||
|
readonlyOnCreate: true,
|
||||||
|
unknownValueLabel: '未知角色',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function define(
|
function define(
|
||||||
name: string,
|
name: string,
|
||||||
title: string,
|
title: string,
|
||||||
@@ -319,9 +336,9 @@ const reason = [f('reason', { required: true })];
|
|||||||
|
|
||||||
export const resources: ResourceUiDefinition[] = [
|
export const resources: ResourceUiDefinition[] = [
|
||||||
{ ...define('gas_basic', '气站管理', 'writable', [f('code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('address'), f('longitude'), f('latitude')]), accountManagement: { resource: '/gas_account', relationKey: 'gas_basic_identity', title: '气站账户' }, walletOwnerType: 'gas' },
|
{ ...define('gas_basic', '气站管理', 'writable', [f('code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('address'), f('longitude'), f('latitude')]), accountManagement: { resource: '/gas_account', relationKey: 'gas_basic_identity', title: '气站账户' }, walletOwnerType: 'gas' },
|
||||||
define('gas_account', '气站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), relation('gas_basic_identity', '/gas_basic', true)]),
|
define('gas_account', '气站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('气站管理员'), relation('gas_basic_identity', '/gas_basic', true)]),
|
||||||
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), f('gas_basic_identity', { label: '气站', listLabel: '气站名称', type: 'identity', relation: '/gas_basic', displayRelationLabel: true, emptyText: '平台直属', placeholder: '请选择气站,留空表示平台直属' }), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' }, walletOwnerType: 'delivery' },
|
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), f('gas_basic_identity', { label: '气站', listLabel: '气站名称', type: 'identity', relation: '/gas_basic', displayRelationLabel: true, emptyText: '平台直属', placeholder: '请选择气站,留空表示平台直属' }), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' }, walletOwnerType: 'delivery' },
|
||||||
define('delivery_account', '配送站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), relation('delivery_basic_identity', '/delivery_basic', true)]),
|
define('delivery_account', '配送点账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('配送点管理员'), relation('delivery_basic_identity', '/delivery_basic', true)]),
|
||||||
{ ...define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })]), walletOwnerType: 'staff' },
|
{ ...define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })]), walletOwnerType: 'staff' },
|
||||||
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
||||||
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
|
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
|
||||||
@@ -418,7 +435,7 @@ export const resources: ResourceUiDefinition[] = [
|
|||||||
define('fin_reconciliation', '财务对账', 'readonly', []),
|
define('fin_reconciliation', '财务对账', 'readonly', []),
|
||||||
define('cms_content', '内容', 'writable', [f('content_type', { required: true }), f('title', { required: true }), f('body', { required: true }), f('version_no'), f('publish_status')]),
|
define('cms_content', '内容', 'writable', [f('content_type', { required: true }), f('title', { required: true }), f('body', { required: true }), f('version_no'), f('publish_status')]),
|
||||||
define('cs_ticket', '客服工单', 'writable', [relation('user_account_identity', '/user_account', true), f('ticket_no', { required: true }), f('category', { required: true }), f('priority', { required: true })]),
|
define('cs_ticket', '客服工单', 'writable', [relation('user_account_identity', '/user_account', true), f('ticket_no', { required: true }), f('category', { required: true }), f('priority', { required: true })]),
|
||||||
define('platform_account', '平台账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('avatar'), f('platform_role_code', { required: true }), f('phone')]),
|
define('platform_account', '平台账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('avatar'), f('platform_role_code', { required: true, unknownValueLabel: '未知角色' }), f('phone')]),
|
||||||
define('platform_role', '平台角色', 'writable', [f('role_code', { required: true }), f('name', { required: true }), f('location_scope', { required: true, type: 'select', options: [{ label: '脱敏坐标', value: 'standard' }, { label: '精确坐标', value: 'precise' }] })], 'list', [
|
define('platform_role', '平台角色', 'writable', [f('role_code', { required: true }), f('name', { required: true }), f('location_scope', { required: true, type: 'select', options: [{ label: '脱敏坐标', value: 'standard' }, { label: '精确坐标', value: 'precise' }] })], 'list', [
|
||||||
{ name: '分配菜单', resource: '/platform_role/:identity/menu', method: 'PUT', fields: [f('menu_identities', { type: 'identity-list', relation: '/platform_menu' })] },
|
{ name: '分配菜单', resource: '/platform_role/:identity/menu', method: 'PUT', fields: [f('menu_identities', { type: 'identity-list', relation: '/platform_menu' })] },
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<!--
|
<!--
|
||||||
功能:展示账户类资源的头像、用户名、唯一标识和创建时间摘要。
|
功能:展示账户类资源的头像、用户名、唯一标识和创建时间摘要。
|
||||||
版本:v1.0.0
|
版本:v1.1.0
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<a-card class="account-card" :bordered="false">
|
<a-card class="account-card" :bordered="false">
|
||||||
<div class="account-summary">
|
<div class="account-summary" :class="{ 'account-summary-text': !avatarEnabled }">
|
||||||
<div class="avatar-column">
|
<div v-if="avatarEnabled" class="avatar-column">
|
||||||
<button
|
<button
|
||||||
class="avatar-button"
|
class="avatar-button"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -106,6 +106,15 @@ function onAvatarSelected(event: Event) {
|
|||||||
min-height: 168px;
|
min-height: 168px;
|
||||||
padding: 18px 32px;
|
padding: 18px 32px;
|
||||||
}
|
}
|
||||||
|
.account-summary-text {
|
||||||
|
grid-template-columns: minmax(420px, 720px);
|
||||||
|
justify-content: start;
|
||||||
|
min-height: 116px;
|
||||||
|
padding: 14px 32px;
|
||||||
|
}
|
||||||
|
.account-summary-text .identity-summary {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.avatar-column {
|
.avatar-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -188,5 +197,10 @@ function onAvatarSelected(event: Event) {
|
|||||||
width: min(100%, 440px);
|
width: min(100%, 440px);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
.account-summary-text {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
min-height: auto;
|
||||||
|
padding: 14px 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!--
|
<!--
|
||||||
功能:以响应式信息卡和子表页签展示标准资源详情。
|
功能:以响应式信息卡和子表页签展示标准资源详情。
|
||||||
版本:v1.1.0
|
版本:v1.2.0
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="detail-stack">
|
<div class="detail-stack">
|
||||||
@@ -58,6 +58,10 @@ const props = defineProps<{
|
|||||||
definition: ResourceUiDefinition;
|
definition: ResourceUiDefinition;
|
||||||
detail: ResourceRow;
|
detail: ResourceRow;
|
||||||
relationOptions: Record<string, ResourceRow[]>;
|
relationOptions: Record<string, ResourceRow[]>;
|
||||||
|
fieldOptions?: Record<
|
||||||
|
string,
|
||||||
|
Array<{ label: string; value: string | number }>
|
||||||
|
>;
|
||||||
accountSummary: boolean;
|
accountSummary: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -97,7 +101,12 @@ const entries = computed<DetailEntry[]>(() => {
|
|||||||
return [];
|
return [];
|
||||||
const field = props.definition.fields.find((item) => item.key === key);
|
const field = props.definition.fields.find((item) => item.key === key);
|
||||||
const display = field
|
const display = field
|
||||||
? displayResourceField(field, row, props.relationOptions)
|
? displayResourceField(
|
||||||
|
field,
|
||||||
|
row,
|
||||||
|
props.relationOptions,
|
||||||
|
props.fieldOptions,
|
||||||
|
)
|
||||||
: displayRawValue(actualKey, value);
|
: displayRawValue(actualKey, value);
|
||||||
const objectValue = typeof value === 'object' && value !== null;
|
const objectValue = typeof value === 'object' && value !== null;
|
||||||
return [
|
return [
|
||||||
@@ -161,7 +170,8 @@ const collections = computed(() =>
|
|||||||
}
|
}
|
||||||
.detail-item {
|
.detail-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 120px minmax(0, 1fr);
|
grid-template-columns: max-content minmax(0, 1fr);
|
||||||
|
column-gap: 12px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
min-height: 38px;
|
min-height: 38px;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
@@ -172,8 +182,7 @@ const collections = computed(() =>
|
|||||||
}
|
}
|
||||||
.detail-label {
|
.detail-label {
|
||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
text-align: right;
|
white-space: nowrap;
|
||||||
padding-right: 18px;
|
|
||||||
}
|
}
|
||||||
.detail-value {
|
.detail-value {
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
@@ -208,7 +217,6 @@ const collections = computed(() =>
|
|||||||
.detail-item,
|
.detail-item,
|
||||||
.detail-item-wide {
|
.detail-item-wide {
|
||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
grid-template-columns: 100px minmax(0, 1fr);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!--
|
<!--
|
||||||
功能:渲染标准资源的新建、编辑和业务动作字段控件。
|
功能:渲染标准资源的新建、编辑和业务动作字段控件。
|
||||||
版本:v1.0.0
|
版本:v1.1.0
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="field-grid">
|
<div class="field-grid">
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
:disabled="disabledSet.has(field.key)"
|
:disabled="disabledSet.has(field.key)"
|
||||||
allow-clear
|
allow-clear
|
||||||
>
|
>
|
||||||
<a-option v-for="option in field.options" :key="option.value" :value="option.value">
|
<a-option v-for="option in selectOptions(field)" :key="option.value" :value="option.value">
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</a-option>
|
</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
@@ -90,9 +90,8 @@
|
|||||||
v-else
|
v-else
|
||||||
v-model="model[field.key]"
|
v-model="model[field.key]"
|
||||||
:disabled="disabledSet.has(field.key)"
|
:disabled="disabledSet.has(field.key)"
|
||||||
:placeholder="disabledSet.has(field.key) ? '创建后不可修改' : `请输入${field.label}`"
|
:placeholder="disabledSet.has(field.key) ? '' : `请输入${field.label}`"
|
||||||
/>
|
/>
|
||||||
<div v-if="disabledSet.has(field.key)" class="readonly-hint">创建后不可修改</div>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -135,6 +134,24 @@ function isWide(field: ResourceField) {
|
|||||||
/(address|terms|content|body|remark|reason|params|args)$/.test(field.key)
|
/(address|terms|content|body|remark|reason|params|args)$/.test(field.key)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 为历史未知编码补充只读选项,避免下拉框退回显示原始值。 */
|
||||||
|
function selectOptions(field: ResourceField) {
|
||||||
|
const options = [...(field.options ?? [])];
|
||||||
|
const value = model.value[field.key];
|
||||||
|
if (
|
||||||
|
field.unknownValueLabel &&
|
||||||
|
value != null &&
|
||||||
|
value !== '' &&
|
||||||
|
!options.some((option) => String(option.value) === String(value))
|
||||||
|
) {
|
||||||
|
options.push({
|
||||||
|
label: `${field.unknownValueLabel}(${String(value)})`,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
@@ -152,11 +169,6 @@ function isWide(field: ResourceField) {
|
|||||||
.field-readonly :deep(.arco-input-number) {
|
.field-readonly :deep(.arco-input-number) {
|
||||||
background: var(--color-fill-1);
|
background: var(--color-fill-1);
|
||||||
}
|
}
|
||||||
.readonly-hint {
|
|
||||||
margin-top: 4px;
|
|
||||||
color: var(--color-text-3);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
:deep(.arco-input-number),
|
:deep(.arco-input-number),
|
||||||
:deep(.arco-picker) {
|
:deep(.arco-picker) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* 功能:定义标准资源独立页面的布局、卡片和响应式样式。
|
* 功能:定义标准资源独立页面的布局、卡片和响应式样式。
|
||||||
* 版本:v1.2.0
|
* 版本:v1.3.0
|
||||||
*/
|
*/
|
||||||
.record-page {
|
.record-page {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -52,7 +52,6 @@
|
|||||||
.form-content {
|
.form-content {
|
||||||
container: record-form / inline-size;
|
container: record-form / inline-size;
|
||||||
}
|
}
|
||||||
.form-alert,
|
|
||||||
.workflow-alert {
|
.workflow-alert {
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!--
|
<!--
|
||||||
功能:承载平台总后台全部标准资源的新建、详情和编辑独立页面。
|
功能:承载平台总后台全部标准资源的新建、详情和编辑独立页面。
|
||||||
版本:v1.2.0
|
版本:v1.3.0
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="record-page">
|
<div class="record-page">
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ResourceAccountSummary
|
<ResourceAccountSummary
|
||||||
v-if="accountSummary"
|
v-if="accountSummaryVisible"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:title="definition.title"
|
:title="definition.title"
|
||||||
:record="summaryRecord"
|
:record="summaryRecord"
|
||||||
@@ -49,6 +49,7 @@
|
|||||||
:definition="definition"
|
:definition="definition"
|
||||||
:detail="detail"
|
:detail="detail"
|
||||||
:relation-options="relations.options"
|
:relation-options="relations.options"
|
||||||
|
:field-options="fieldOptions"
|
||||||
:account-summary="accountSummary"
|
:account-summary="accountSummary"
|
||||||
/>
|
/>
|
||||||
<ResourceWalletSummary
|
<ResourceWalletSummary
|
||||||
@@ -93,9 +94,6 @@
|
|||||||
<div class="form-shell">基本信息</div>
|
<div class="form-shell">基本信息</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="form-shell form-content">
|
<div class="form-shell form-content">
|
||||||
<a-alert v-if="mode === 'edit' && readonlyKeys.length" class="form-alert" type="info" show-icon>
|
|
||||||
灰色字段为创建后不可修改的信息,不会提交到更新接口。
|
|
||||||
</a-alert>
|
|
||||||
<a-form :model="form" layout="vertical">
|
<a-form :model="form" layout="vertical">
|
||||||
<ResourceFieldForm
|
<ResourceFieldForm
|
||||||
v-model="form"
|
v-model="form"
|
||||||
@@ -196,6 +194,9 @@ const errorMessage = ref('');
|
|||||||
const errorStatus = ref<'403' | '404' | 'error'>('error');
|
const errorStatus = ref<'403' | '404' | 'error'>('error');
|
||||||
const wallet = ref<ResourceRow>();
|
const wallet = ref<ResourceRow>();
|
||||||
const roleOptions = ref<PlatformRole[]>([]);
|
const roleOptions = ref<PlatformRole[]>([]);
|
||||||
|
const fieldOptions = reactive<
|
||||||
|
Record<string, Array<{ label: string; value: string | number }>>
|
||||||
|
>({});
|
||||||
const relations = useResourceRelations();
|
const relations = useResourceRelations();
|
||||||
const actionVisible = ref(false);
|
const actionVisible = ref(false);
|
||||||
const activeAction = ref<DetailAction>();
|
const activeAction = ref<DetailAction>();
|
||||||
@@ -206,6 +207,11 @@ const selectAvatar = avatar.select;
|
|||||||
const clearAvatar = avatar.clear;
|
const clearAvatar = avatar.clear;
|
||||||
|
|
||||||
const accountSummary = computed(() => usesAccountSummary(definition.value));
|
const accountSummary = computed(() => usesAccountSummary(definition.value));
|
||||||
|
const accountSummaryVisible = computed(
|
||||||
|
() =>
|
||||||
|
accountSummary.value &&
|
||||||
|
(mode.value !== 'create' || hasAvatarField.value),
|
||||||
|
);
|
||||||
const hasAvatarField = computed(() =>
|
const hasAvatarField = computed(() =>
|
||||||
definition.value.fields.some((field) => field.key === 'avatar'),
|
definition.value.fields.some((field) => field.key === 'avatar'),
|
||||||
);
|
);
|
||||||
@@ -235,7 +241,11 @@ const editableKeySet = computed(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
const readonlyKeys = computed(() =>
|
const readonlyKeys = computed(() =>
|
||||||
mode.value === 'edit'
|
mode.value === 'create'
|
||||||
|
? formFields.value
|
||||||
|
.filter((field) => field.readonlyOnCreate)
|
||||||
|
.map((field) => field.key)
|
||||||
|
: mode.value === 'edit'
|
||||||
? formFields.value
|
? formFields.value
|
||||||
.filter((field) => !editableKeySet.value.has(field.key))
|
.filter((field) => !editableKeySet.value.has(field.key))
|
||||||
.map((field) => field.key)
|
.map((field) => field.key)
|
||||||
@@ -353,9 +363,19 @@ async function loadRelationsAndRoles() {
|
|||||||
if (
|
if (
|
||||||
definition.value.fields.some((field) => field.key === 'platform_role_code')
|
definition.value.fields.some((field) => field.key === 'platform_role_code')
|
||||||
) {
|
) {
|
||||||
roleOptions.value = (await platformApi.listRole()).list.filter(
|
try {
|
||||||
|
const roles = (await platformApi.listRole()).list;
|
||||||
|
fieldOptions.platform_role_code = roles.map((role) => ({
|
||||||
|
label: role.name,
|
||||||
|
value: role.role_code,
|
||||||
|
}));
|
||||||
|
roleOptions.value = roles.filter(
|
||||||
(role) => !role.is_system && role.status === 1,
|
(role) => !role.is_system && role.status === 1,
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
fieldOptions.platform_role_code = [];
|
||||||
|
Message.warning(`平台角色加载失败:${(error as Error).message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
v-if="field.type === 'identity' && !field.displayRelationLabel && identityFieldValue(field, record)"
|
v-if="field.type === 'identity' && !field.displayRelationLabel && identityFieldValue(field, record)"
|
||||||
:value="identityFieldValue(field, record)"
|
:value="identityFieldValue(field, record)"
|
||||||
/>
|
/>
|
||||||
<template v-else>{{ displayResourceField(field, record, relations.options) }}</template>
|
<template v-else>{{ displayListField(field, record) }}</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column v-if="definition.accountManagement" title="账户数" :width="90">
|
<a-table-column v-if="definition.accountManagement" title="账户数" :width="90">
|
||||||
@@ -148,6 +148,7 @@ import {
|
|||||||
import { editBlockedReason, type ResourceRow } from '@/api/resource-page-rules';
|
import { editBlockedReason, type ResourceRow } from '@/api/resource-page-rules';
|
||||||
import { safeReturnPath } from '@/api/resource-navigation';
|
import { safeReturnPath } from '@/api/resource-navigation';
|
||||||
import { resourceApi } from '@/api/resource';
|
import { resourceApi } from '@/api/resource';
|
||||||
|
import { platformApi } from '@/api/platform';
|
||||||
import type { ResourceField, ResourceUiDefinition } from '@/api/resources';
|
import type { ResourceField, ResourceUiDefinition } from '@/api/resources';
|
||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store';
|
||||||
import { useResourceRelations } from '../resource/use-resource-relations';
|
import { useResourceRelations } from '../resource/use-resource-relations';
|
||||||
@@ -167,6 +168,9 @@ const filters = reactive({
|
|||||||
keyword: typeof route.query.keyword === 'string' ? route.query.keyword : '',
|
keyword: typeof route.query.keyword === 'string' ? route.query.keyword : '',
|
||||||
});
|
});
|
||||||
const relations = useResourceRelations();
|
const relations = useResourceRelations();
|
||||||
|
const fieldOptions = reactive<
|
||||||
|
Record<string, Array<{ label: string; value: string | number }>>
|
||||||
|
>({});
|
||||||
const extras = useResourceListExtras(definitionRef, list);
|
const extras = useResourceListExtras(definitionRef, list);
|
||||||
const statusVisible = ref(false);
|
const statusVisible = ref(false);
|
||||||
const statusSaving = ref(false);
|
const statusSaving = ref(false);
|
||||||
@@ -426,8 +430,38 @@ function columnWidth(field: ResourceField) {
|
|||||||
return 180;
|
return 180;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 使用资源静态选项或运行时平台角色名称显示列表字段。 */
|
||||||
|
function displayListField(field: ResourceField, row: ResourceRow) {
|
||||||
|
return displayResourceField(field, row, relations.options, fieldOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 平台角色由后端维护,列表加载全部角色用于编码转中文名称。 */
|
||||||
|
async function loadFieldOptions() {
|
||||||
|
if (
|
||||||
|
!props.definition.fields.some(
|
||||||
|
(field) => field.key === 'platform_role_code',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
delete fieldOptions.platform_role_code;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const roles = (await platformApi.listRole()).list;
|
||||||
|
fieldOptions.platform_role_code = roles.map((role) => ({
|
||||||
|
label: role.name,
|
||||||
|
value: role.role_code,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
fieldOptions.platform_role_code = [];
|
||||||
|
Message.warning(`平台角色加载失败:${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await relations.preload(displayFields.value);
|
await Promise.all([
|
||||||
|
relations.preload(displayFields.value),
|
||||||
|
loadFieldOptions(),
|
||||||
|
]);
|
||||||
await load();
|
await load();
|
||||||
});
|
});
|
||||||
watch(
|
watch(
|
||||||
@@ -435,7 +469,10 @@ watch(
|
|||||||
async () => {
|
async () => {
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
filters.keyword = '';
|
filters.keyword = '';
|
||||||
await relations.preload(displayFields.value);
|
await Promise.all([
|
||||||
|
relations.preload(displayFields.value),
|
||||||
|
loadFieldOptions(),
|
||||||
|
]);
|
||||||
await load();
|
await load();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user