适配气站工作人员与用户受保护头像
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/upload"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -52,15 +53,31 @@ func GetStaff(ctx *gin.Context) {
|
||||
Where("identity = ? AND gas_basic_id = ?", ctx.Param("identity"), station.ID), &models.StaffAccount{})
|
||||
}
|
||||
|
||||
// GetStaffAvatar 返回当前气站范围内工作人员的受保护头像。
|
||||
func GetStaffAvatar(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var account models.StaffAccount
|
||||
if err := common.ActiveRecords(impl.DBService).Select("avatar").
|
||||
Where("identity = ? AND gas_basic_id = ?", ctx.Param("identity"), station.ID).
|
||||
First(&account).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
upload.ServeAvatar(ctx, account.Avatar)
|
||||
}
|
||||
|
||||
type staffRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Avatar string `json:"avatar" binding:"max=512"`
|
||||
RoleCode string `json:"role_code" binding:"required"`
|
||||
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
|
||||
WorkStatus string `json:"work_status" binding:"required"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Avatar *string `json:"avatar" binding:"omitempty,max=512"`
|
||||
RoleCode string `json:"role_code" binding:"required"`
|
||||
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
|
||||
WorkStatus string `json:"work_status" binding:"required"`
|
||||
}
|
||||
|
||||
func CreateStaff(ctx *gin.Context) {
|
||||
@@ -87,9 +104,13 @@ func CreateStaff(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
avatar := ""
|
||||
if request.Avatar != nil {
|
||||
avatar = *request.Avatar
|
||||
}
|
||||
staff := models.StaffAccount{
|
||||
Entity: common.NewEntity(common.StatusEnable), Username: request.Username, PasswordHash: hash,
|
||||
Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RoleCode: request.RoleCode,
|
||||
Name: request.Name, Phone: request.Phone, Avatar: avatar, RoleCode: request.RoleCode,
|
||||
GasBasicID: station.ID, DeliveryBasicID: deliveryID, WorkStatus: request.WorkStatus,
|
||||
}
|
||||
if err := common.CreateStaffRecord(&staff); err != nil {
|
||||
@@ -120,10 +141,15 @@ func UpdateStaff(ctx *gin.Context) {
|
||||
}
|
||||
deliveryID = delivery.ID
|
||||
}
|
||||
common.UpdateAllowedByIdentityWithError(ctx, &models.StaffAccount{}, gin.H{
|
||||
"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "role_code": request.RoleCode,
|
||||
values := gin.H{
|
||||
"name": request.Name, "phone": request.Phone, "role_code": request.RoleCode,
|
||||
"delivery_basic_id": deliveryID, "work_status": request.WorkStatus,
|
||||
}, []string{"name", "phone", "avatar", "role_code", "delivery_basic_id", "work_status"}, common.StaffWriteError)
|
||||
}
|
||||
// 未选择新头像时不提交 avatar,避免编辑基础资料误清空现有头像。
|
||||
if request.Avatar != nil {
|
||||
values["avatar"] = *request.Avatar
|
||||
}
|
||||
common.UpdateAllowedByIdentityWithError(ctx, &models.StaffAccount{}, values, []string{"name", "phone", "avatar", "role_code", "delivery_basic_id", "work_status"}, common.StaffWriteError)
|
||||
}
|
||||
|
||||
func UpdateStaffStatus(ctx *gin.Context) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/upload"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -57,14 +58,27 @@ func GetUser(ctx *gin.Context) {
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
|
||||
// GetUserAvatar 返回与当前气站存在服务关系的用户受保护头像。
|
||||
func GetUserAvatar(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
user, _, ok := requireUser(ctx, ctx.Param("identity"), station.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
upload.ServeAvatar(ctx, user.Avatar)
|
||||
}
|
||||
|
||||
type userRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Avatar string `json:"avatar" binding:"max=512"`
|
||||
RealName string `json:"real_name" binding:"max=64"`
|
||||
DeliveryBasicIdentity string `json:"delivery_basic_identity" binding:"required"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Avatar *string `json:"avatar" binding:"omitempty,max=512"`
|
||||
RealName string `json:"real_name" binding:"max=64"`
|
||||
DeliveryBasicIdentity string `json:"delivery_basic_identity" binding:"required"`
|
||||
}
|
||||
|
||||
func CreateUser(ctx *gin.Context) {
|
||||
@@ -86,9 +100,13 @@ func CreateUser(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
avatar := ""
|
||||
if request.Avatar != nil {
|
||||
avatar = *request.Avatar
|
||||
}
|
||||
user := models.UserAccount{
|
||||
Entity: common.NewEntity(common.StatusEnable), Username: request.Username, PasswordHash: hash,
|
||||
Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RealName: request.RealName,
|
||||
Name: request.Name, Phone: request.Phone, Avatar: avatar, RealName: request.RealName,
|
||||
}
|
||||
relation := models.UserServiceRelation{Entity: common.NewEntity(common.StatusEnable), GasBasicID: station.ID, DeliveryBasicID: delivery.ID}
|
||||
if err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
@@ -123,9 +141,14 @@ func UpdateUser(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&user).Updates(map[string]any{
|
||||
"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "real_name": request.RealName,
|
||||
}).Error; err != nil {
|
||||
values := map[string]any{
|
||||
"name": request.Name, "phone": request.Phone, "real_name": request.RealName,
|
||||
}
|
||||
// 未选择新头像时不提交 avatar,避免编辑基础资料误清空现有头像。
|
||||
if request.Avatar != nil {
|
||||
values["avatar"] = *request.Avatar
|
||||
}
|
||||
if err := tx.Model(&user).Updates(values).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&relation).Update("delivery_basic_id", delivery.ID).Error
|
||||
|
||||
@@ -30,6 +30,7 @@ func registerGasBusinessRoutes(group *gin.RouterGroup) {
|
||||
staff.GET("", gaslogic.ListStaff)
|
||||
staff.POST("", gaslogic.CreateStaff)
|
||||
staff.GET("/:identity", gaslogic.GetStaff)
|
||||
staff.GET("/:identity/avatar", gaslogic.GetStaffAvatar)
|
||||
staff.PUT("/:identity", gaslogic.UpdateStaff)
|
||||
staff.PUT("/:identity/password", gaslogic.ResetStaffPassword)
|
||||
staff.PATCH("/:identity/status", gaslogic.UpdateStaffStatus)
|
||||
@@ -47,6 +48,7 @@ func registerGasBusinessRoutes(group *gin.RouterGroup) {
|
||||
user.GET("", gaslogic.ListUser)
|
||||
user.POST("", gaslogic.CreateUser)
|
||||
user.GET("/:identity", gaslogic.GetUser)
|
||||
user.GET("/:identity/avatar", gaslogic.GetUserAvatar)
|
||||
user.PUT("/:identity", gaslogic.UpdateUser)
|
||||
user.PUT("/:identity/password", gaslogic.ResetUserPassword)
|
||||
user.PATCH("/:identity/status", gaslogic.UpdateUserStatus)
|
||||
|
||||
@@ -81,3 +81,22 @@ func TestGasReadonlyDetailRoutes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGasAvatarRoutes 验证工作人员与用户头像只能通过气站鉴权路由读取。
|
||||
func TestGasAvatarRoutes(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
RegisterGas("heqi", engine)
|
||||
routes := map[string]bool{}
|
||||
for _, route := range engine.Routes() {
|
||||
routes[route.Method+" "+route.Path] = true
|
||||
}
|
||||
for _, path := range []string{
|
||||
"GET /heqi/gas/v1/staff_account/:identity/avatar",
|
||||
"GET /heqi/gas/v1/user_account/:identity/avatar",
|
||||
} {
|
||||
if !routes[path] {
|
||||
t.Fatalf("missing gas avatar route %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
docs/操作日志_气站工作人员头像适配_20260818.md
Normal file
49
docs/操作日志_气站工作人员头像适配_20260818.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# 气站工作人员头像适配操作日志
|
||||
|
||||
操作时间:2026-08-18
|
||||
操作类型:扩展
|
||||
影响模块:气站管理端账户记录页、气站 API 工作人员与用户头像
|
||||
|
||||
## 操作前状态
|
||||
|
||||
- 5175 工作人员新建页把头像显示为普通文本输入框。
|
||||
- 气站 API 没有工作人员和用户的受保护头像读取路由。
|
||||
- 编辑工作人员或用户资料时,未提交头像也可能把已有头像清空。
|
||||
|
||||
## 具体操作
|
||||
|
||||
- 复用 5173 的账户摘要组件和头像组合式逻辑,在新建、详情、编辑页显示头像选择与预览。
|
||||
- 启用工作人员与用户列表的受保护头像缩略图,替代头像存储路径文本。
|
||||
- 从基础信息表单移除头像文本输入框,保存时仅在选择新头像或恢复默认头像后提交头像字段。
|
||||
- 新增工作人员、用户头像读取路由,查询始终附加当前气站或服务关系范围。
|
||||
- 将更新请求的头像字段改为可选指针,未修改头像时不更新数据库列。
|
||||
|
||||
## 操作后状态
|
||||
|
||||
- `/staff/add` 顶部显示账户摘要与头像选择区,布局和交互参考 5173。
|
||||
- 工作人员与用户列表显示 32 像素圆形头像;无头像或加载失败时回退默认头像。
|
||||
- 工作人员与用户的详情、编辑页可安全读取现有头像。
|
||||
- 普通资料编辑不会误清空头像;主动恢复默认头像仍会提交空值。
|
||||
|
||||
## 代码变更
|
||||
|
||||
- `frontend/gas_admin/src/views/resource/ResourceRecordPage.vue`:启用头像摘要、过滤文本字段、接入上传载荷与未保存检测。
|
||||
- `frontend/gas_admin/src/api/avatar.ts`:使用气站 API 前缀读取受保护头像。
|
||||
- `frontend/gas_admin/src/views/shared/protected-list-avatar-loader.ts`:开放工作人员与用户缩略图渲染。
|
||||
- `backend/api/internal/logic/gas/staff.go`、`user.go`:新增范围受限头像读取并保护头像更新语义。
|
||||
- `backend/api/internal/routers/gas_business.go`、`gas_test.go`:注册并校验头像路由。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- `go test ./internal/routers ./internal/logic/gas`:通过。
|
||||
- `pnpm type:check`:通过。
|
||||
- `pnpm contract:check`:通过,20 个资源一致。
|
||||
- `pnpm resource-pages:check`:通过,详情 20、新建 11、编辑 8。
|
||||
- `pnpm lint`:通过,无错误;保留仓库既有提示。
|
||||
- `pnpm build`:通过。
|
||||
|
||||
## 风险评估
|
||||
|
||||
- 新头像接口只读取数据库中受控上传目录的文件,不暴露外部 URL 或任意文件路径。
|
||||
- 工作人员头像按 `gas_basic_id` 限制,用户头像按当前气站服务关系限制,未扩大数据范围。
|
||||
- 合同附件能力未变更,避免气站端调用不存在的平台专属接口。
|
||||
@@ -23,7 +23,7 @@
|
||||
- 20 类标准资源具有独立详情页。
|
||||
- 11 类资源具有独立新建页,8 类资源具有独立编辑页。
|
||||
- `/staff/add` 和 `/gasorder/create` 原地址继续可用。
|
||||
- 头像和合同文件字段保持 5175 原能力,不访问平台专属端点。
|
||||
- 初次改造时头像和合同文件字段保持 5175 原能力;2026-08-18 已按 5173 补齐气站范围内头像能力,合同文件仍不访问平台专属端点。
|
||||
|
||||
## 代码变更
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ platforms/
|
||||
- 保留配送点、工作人员、用户、合同、订单、钱包和工单的数据范围。
|
||||
- 保留密码重置、合同启用/续签/终止、合同气瓶解绑和气站订单动作。
|
||||
- 工作人员资质按来源角色校验;订单分配只加载配送人员。
|
||||
- 头像和 `file_uri` 继续作为普通字段,不调用平台总后台专属的受控头像或合同附件接口。
|
||||
- 工作人员和用户头像使用 5173 同款账户摘要、选择预览和保存流程,并通过气站数据范围内的受控接口读取;`file_uri` 仍保持普通字段。
|
||||
- 支付与退款资源使用后端真实名称 `payment_order`、`payment_refund`。
|
||||
|
||||
### 4.4 只读详情接口
|
||||
@@ -75,6 +75,7 @@ platforms/
|
||||
- 列表的新建、详情和编辑入口改为路由跳转。
|
||||
- 新增 11 类受数据范围保护的详情接口及路由测试。
|
||||
- 同步气站资源契约和独立页面覆盖检查。
|
||||
- 为工作人员与用户补充受气站范围保护的头像读取接口,并避免普通资料编辑误清空头像。
|
||||
- 未修改数据库模型、状态机、金额规则或现有写接口。
|
||||
|
||||
## 6. 维护指南
|
||||
@@ -87,6 +88,6 @@ platforms/
|
||||
|
||||
## 7. 已知边界
|
||||
|
||||
- 气站 API 暂无受控头像读取和合同附件预览接口,因此对应字段保持原普通文本能力。
|
||||
- 气站 API 暂无合同附件预览接口,因此 `file_uri` 仍保持原普通文本能力。
|
||||
- 财务对账模型没有气站主体归属字段,气站端仍不展示全局对账数据。
|
||||
- 两个共享页面文件超过 500 行,但按列表编排与记录页编排保持单一职责;后续若继续增加气站专属区块,应优先拆分组合式函数或子组件。
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* 功能:平台总后台头像上传与鉴权读取客户端。
|
||||
* 版本:v1.1.0
|
||||
* 功能:气站管理端头像上传与气站范围内鉴权读取客户端。
|
||||
* 版本:v1.2.0
|
||||
*/
|
||||
import { getToken } from '@/utils/auth';
|
||||
|
||||
const platformApiBaseURL =
|
||||
const gasApiBaseURL =
|
||||
import.meta.env.VITE_API_BASE_URL ||
|
||||
'http://localhost:12426/heqi/platform/v1';
|
||||
'http://localhost:12426/heqi/gas/v1';
|
||||
|
||||
export type AvatarUploadReply = {
|
||||
uri: string;
|
||||
@@ -17,13 +17,13 @@ export type AvatarUploadReply = {
|
||||
|
||||
type ApiEnvelope<T> = { code?: number; message?: string; details?: T };
|
||||
|
||||
/** 生成服务根路径 URL,确保上传请求不会错误拼接平台 API 前缀。 */
|
||||
/** 生成服务根路径 URL,确保上传请求不会错误拼接气站 API 前缀。 */
|
||||
function serviceURL(path: string) {
|
||||
const platformURL = new URL(platformApiBaseURL, window.location.origin);
|
||||
return new URL(path, platformURL.origin).toString();
|
||||
const gasURL = new URL(gasApiBaseURL, window.location.origin);
|
||||
return new URL(path, gasURL.origin).toString();
|
||||
}
|
||||
|
||||
/** 返回与现有平台请求一致的 JWT 请求头。 */
|
||||
/** 返回与现有气站请求一致的 JWT 请求头。 */
|
||||
function authorizationHeaders(): Record<string, string> {
|
||||
const token = getToken();
|
||||
return token ? { Authorization: token } : {};
|
||||
@@ -52,7 +52,7 @@ async function load(
|
||||
signal?: AbortSignal,
|
||||
): Promise<Blob | undefined> {
|
||||
const response = await fetch(
|
||||
`${platformApiBaseURL}${resource}/${encodeURIComponent(identity)}/avatar`,
|
||||
`${gasApiBaseURL}${resource}/${encodeURIComponent(identity)}/avatar`,
|
||||
{ headers: authorizationHeaders(), signal },
|
||||
);
|
||||
if (response.status === 404) return undefined;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!--
|
||||
功能:承载气站管理端全部标准资源的新建、详情和编辑独立页面。
|
||||
版本:v1.3.0
|
||||
版本:v1.4.0
|
||||
-->
|
||||
<template>
|
||||
<div class="record-page">
|
||||
@@ -37,9 +37,11 @@
|
||||
:mode="mode"
|
||||
:title="definition.title"
|
||||
:record="summaryRecord"
|
||||
avatar-url=""
|
||||
:avatar-enabled="false"
|
||||
:can-clear="false"
|
||||
:avatar-url="avatarUrl"
|
||||
:avatar-enabled="hasAvatarField"
|
||||
:can-clear="avatarCanClear"
|
||||
@select-avatar="selectAvatar"
|
||||
@clear-avatar="clearAvatar"
|
||||
/>
|
||||
|
||||
<template v-if="mode === 'detail'">
|
||||
@@ -196,6 +198,7 @@ import ResourceFieldForm from './ResourceFieldForm.vue';
|
||||
import ResourceWalletSummary from './ResourceWalletSummary.vue';
|
||||
import { createResourceRecordNavigation } from './resource-record-navigation';
|
||||
import { loadResourceRecordRelations } from './load-resource-record-relations';
|
||||
import { useResourceAvatar } from './use-resource-avatar';
|
||||
import { useResourceRelationLinkage } from './use-resource-relation-linkage';
|
||||
import { useStaffCredentialOwnerGuard } from './use-staff-credential-owner-guard';
|
||||
import { useUnsavedRecord } from './use-unsaved-record';
|
||||
@@ -253,6 +256,11 @@ const productOwnershipKeys = [
|
||||
];
|
||||
const actionVisible = ref(false);
|
||||
const activeAction = ref<DetailAction>();
|
||||
const avatar = useResourceAvatar();
|
||||
const avatarUrl = avatar.url;
|
||||
const avatarCanClear = avatar.canClear;
|
||||
const selectAvatar = avatar.select;
|
||||
const clearAvatar = avatar.clear;
|
||||
|
||||
/** 新建智能气阀选择归属时清空其他互斥归属,再执行字段原有联动。 */
|
||||
async function changeRelation(field: ResourceField, value: unknown) {
|
||||
@@ -273,7 +281,10 @@ async function changeRelation(field: ResourceField, value: unknown) {
|
||||
const accountSummary = computed(() => usesAccountSummary(definition.value));
|
||||
const accountSummaryVisible = computed(
|
||||
() =>
|
||||
accountSummary.value && mode.value !== 'create',
|
||||
accountSummary.value && (mode.value !== 'create' || hasAvatarField.value),
|
||||
);
|
||||
const hasAvatarField = computed(() =>
|
||||
definition.value.fields.some((field) => field.key === 'avatar'),
|
||||
);
|
||||
const formFields = computed(() =>
|
||||
mode.value === 'detail'
|
||||
@@ -283,11 +294,13 @@ const formFields = computed(() =>
|
||||
mode.value,
|
||||
record.value,
|
||||
context.value,
|
||||
),
|
||||
).filter((field) => field.key !== 'avatar'),
|
||||
);
|
||||
const payloadFields = computed(() =>
|
||||
mode.value === 'edit'
|
||||
? updatePayloadFields(definition.value, record.value, context.value)
|
||||
? updatePayloadFields(definition.value, record.value, context.value).filter(
|
||||
(field) => field.key !== 'avatar',
|
||||
)
|
||||
: formFields.value,
|
||||
);
|
||||
const editableKeySet = computed(
|
||||
@@ -364,7 +377,7 @@ const modeLabel = computed(() =>
|
||||
const errorTitle = computed(() => recordPageErrorTitle(errorStatus.value));
|
||||
|
||||
function snapshot() {
|
||||
return JSON.stringify({ form });
|
||||
return JSON.stringify({ form, avatar: avatar.marker() });
|
||||
}
|
||||
const unsaved = useUnsavedRecord(snapshot, () => mode.value !== 'detail');
|
||||
const { goEdit, viewWallet, goBack, requestBack } =
|
||||
@@ -429,8 +442,8 @@ async function initialize() {
|
||||
fieldOptions,
|
||||
roleOptions,
|
||||
});
|
||||
// 钱包属于附加信息,读取失败时不能阻断主详情或基础表单。
|
||||
await Promise.allSettled([loadWallet()]);
|
||||
// 钱包和头像属于附加信息,读取失败时不能阻断主详情或基础表单。
|
||||
await Promise.allSettled([loadWallet(), loadAvatar()]);
|
||||
unsaved.markInitialized();
|
||||
} catch (error) {
|
||||
const message = (error as Error).message;
|
||||
@@ -463,6 +476,13 @@ async function loadWallet() {
|
||||
).list[0];
|
||||
}
|
||||
|
||||
/** 读取当前气站范围内账户的受保护头像。 */
|
||||
async function loadAvatar() {
|
||||
if (!hasAvatarField.value || mode.value === 'create' || !recordIdentity.value)
|
||||
return;
|
||||
await avatar.load(definition.value.resource, recordIdentity.value);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const validationError = validateResourceRecordForm(
|
||||
form,
|
||||
@@ -485,6 +505,7 @@ async function save() {
|
||||
form,
|
||||
mode.value as 'create' | 'edit',
|
||||
);
|
||||
await avatar.applyToPayload(payload);
|
||||
if (mode.value === 'create') {
|
||||
const created = await resourceApi.create<ResourceRow>(
|
||||
definition.value.resource,
|
||||
@@ -539,7 +560,7 @@ async function reloadDetail() {
|
||||
definition.value.resource,
|
||||
identity.value,
|
||||
);
|
||||
await Promise.allSettled([loadWallet()]);
|
||||
await Promise.allSettled([loadWallet(), loadAvatar()]);
|
||||
}
|
||||
|
||||
async function updateGasStatus(enabled: string | number | boolean) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* 功能:为受控账户列表提供头像请求限流、当前页缓存和取消能力。
|
||||
* 版本:v1.0.0
|
||||
* 版本:v1.1.0
|
||||
*/
|
||||
import { avatarApi } from '@/api/avatar';
|
||||
|
||||
const MAX_CONCURRENT_REQUESTS = 6;
|
||||
// 气站 API 暂无受控头像读取接口,头像继续按普通字段展示。
|
||||
const SUPPORTED_RESOURCES = new Set<string>();
|
||||
// 仅启用已具备气站范围鉴权读取接口的真实头像资源。
|
||||
const SUPPORTED_RESOURCES = new Set(['staff_account', 'user_account']);
|
||||
|
||||
type QueueTask = {
|
||||
run: () => void;
|
||||
|
||||
38
frontend/gas_admin/设计验收_工作人员头像适配.md
Normal file
38
frontend/gas_admin/设计验收_工作人员头像适配.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Design QA
|
||||
|
||||
- source visual truth path: `C:/Users/Lenovo/AppData/Local/Temp/codex-clipboard-1e3ee2da-a03c-4ade-af5d-81e88bacdc3e.png`
|
||||
- implementation screenshot path: unavailable
|
||||
- viewport: source image 2560 × 1159 px;实现页需保持同一浏览器窗口与登录态
|
||||
- source and implementation pixel dimensions: source 2560 × 1159 px;implementation unavailable
|
||||
- CSS size and density normalization: blocked because the authenticated implementation could not be captured
|
||||
- state: 5175 工作人员列表,安装人员菜单,单条带头像记录
|
||||
|
||||
## Full-view comparison evidence
|
||||
|
||||
源图显示头像列直接暴露 `/uploads/avatars/...` 存储路径。代码已按 5173 开启现有 `ProtectedAvatarThumbnail` 分支,目标表现为 32 像素圆形头像;但缺少登录后的实现截图,不能完成视觉对照。
|
||||
|
||||
## Focused region comparison evidence
|
||||
|
||||
头像列是唯一变更区域。组件沿用 5173 的圆形尺寸、默认头像、懒加载、失败提示和对象 URL 清理逻辑;仍需同一数据记录的刷新后截图作为最终证据。
|
||||
|
||||
## Findings
|
||||
|
||||
- [P1] 缺少登录态实现截图
|
||||
- Location: `/staff/installers` 头像列
|
||||
- Evidence: 只有修改前源图,没有刷新后的实现图
|
||||
- Impact: 无法确认运行中的后端已重启并返回头像二进制,也无法确认最终列对齐
|
||||
- Fix: 重启后端、刷新页面并捕获同一视口截图
|
||||
|
||||
## Comparison history
|
||||
|
||||
- Iteration 1: 定位为空的受保护资源集合,已加入 `staff_account` 与 `user_account`;自动化构建验证通过,视觉证据待补。
|
||||
|
||||
## Implementation checklist
|
||||
|
||||
- [x] 工作人员列表启用受保护头像缩略图
|
||||
- [x] 用户列表启用受保护头像缩略图
|
||||
- [x] 无头像时回退默认头像
|
||||
- [x] 类型检查、单文件 lint、页面覆盖检查与生产构建
|
||||
- [ ] 登录态同视口截图对照
|
||||
|
||||
final result: blocked
|
||||
Reference in New Issue
Block a user