feat(content): 首页更多区域模块化配置
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
- home_navigations 表新增 Position 和 ActionType 字段 - 小程序 API 支持按 position 筛选导航列表 - 首页拆分专业测评和更多区域,动态渲染+QR弹窗 - 后台管理支持 Position/ActionType 配置和筛选 - ActionType=1 时 LinkUrl 必填验证 - 状态简化为 0=禁用/1=启用
This commit is contained in:
parent
0ee0870198
commit
9d4f9a0722
|
|
@ -0,0 +1,22 @@
|
|||
-- ============================================================
|
||||
-- 迁移脚本:home_navigations 表新增 Position 和 ActionType 列
|
||||
-- 功能:首页「更多」区域模块化配置
|
||||
-- ============================================================
|
||||
|
||||
-- 1. 新增列
|
||||
ALTER TABLE home_navigations ADD Position INT NOT NULL DEFAULT 1;
|
||||
ALTER TABLE home_navigations ADD ActionType INT NOT NULL DEFAULT 1;
|
||||
|
||||
-- 2. 现有记录设置为专业测评区域 + 跳转页面
|
||||
UPDATE home_navigations SET Position = 1, ActionType = 1 WHERE IsDeleted = 0;
|
||||
|
||||
-- 3. 将原 Status=0 的"即将上线"记录改为 ActionType=3, Status=1
|
||||
UPDATE home_navigations SET ActionType = 3, Status = 1 WHERE Status = 0 AND IsDeleted = 0;
|
||||
-- 注意:Status=2 的记录也改为 ActionType=3
|
||||
UPDATE home_navigations SET ActionType = 3, Status = 1 WHERE Status = 2 AND IsDeleted = 0;
|
||||
|
||||
-- 4. 插入「更多」区域初始数据
|
||||
INSERT INTO home_navigations (Name, ImageUrl, LinkUrl, Sort, Status, Position, ActionType, CreateTime, UpdateTime, IsDeleted)
|
||||
VALUES
|
||||
(N'学习方案', NULL, NULL, 2, 1, 2, 3, GETDATE(), GETDATE(), 0),
|
||||
(N'详细咨询', NULL, NULL, 1, 1, 2, 2, GETDATE(), GETDATE(), 0);
|
||||
|
|
@ -440,6 +440,12 @@ public class ContentController : BusinessControllerBase
|
|||
return ValidationError("导航名称不能为空");
|
||||
}
|
||||
|
||||
// ActionType=1(跳转页面)时 LinkUrl 必填
|
||||
if (request.ActionType == 1 && string.IsNullOrWhiteSpace(request.LinkUrl))
|
||||
{
|
||||
return Error(ErrorCodes.ParamError, "跳转链接不能为空");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var id = await _contentService.CreateNavigationAsync(request);
|
||||
|
|
@ -474,6 +480,12 @@ public class ContentController : BusinessControllerBase
|
|||
return ValidationError("导航名称不能为空");
|
||||
}
|
||||
|
||||
// ActionType=1(跳转页面)时 LinkUrl 必填
|
||||
if (request.ActionType == 1 && string.IsNullOrWhiteSpace(request.LinkUrl))
|
||||
{
|
||||
return Error(ErrorCodes.ParamError, "跳转链接不能为空");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _contentService.UpdateNavigationAsync(request);
|
||||
|
|
@ -532,9 +544,9 @@ public class ContentController : BusinessControllerBase
|
|||
return ValidationError("导航ID无效");
|
||||
}
|
||||
|
||||
if (request.Status < 0 || request.Status > 2)
|
||||
if (request.Status != 0 && request.Status != 1)
|
||||
{
|
||||
return ValidationError("状态值无效,只能为0(下线)、1(上线)或2(即将上线)");
|
||||
return Error(ErrorCodes.ParamError, "状态值无效");
|
||||
}
|
||||
|
||||
try
|
||||
|
|
|
|||
|
|
@ -41,7 +41,17 @@ public class HomeNavigation
|
|||
public int Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态:0即将上线 1已上线
|
||||
/// 区域标识:1=专业测评区域,2=更多区域
|
||||
/// </summary>
|
||||
public int Position { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 动作类型:1=跳转页面,2=弹客服二维码,3=即将上线
|
||||
/// </summary>
|
||||
public int ActionType { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 状态:0=禁用,1=启用
|
||||
/// </summary>
|
||||
public int Status { get; set; } = 1;
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,17 @@ public class CreateHomeNavigationRequest
|
|||
public int Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态:0即将上线 1已上线
|
||||
/// 区域标识:1=专业测评区域,2=更多区域(默认1)
|
||||
/// </summary>
|
||||
public int Position { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 动作类型:1=跳转页面,2=弹客服二维码,3=即将上线(默认1)
|
||||
/// </summary>
|
||||
public int ActionType { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 状态:0=禁用,1=启用
|
||||
/// </summary>
|
||||
public int Status { get; set; } = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,27 @@ public class HomeNavigationDto
|
|||
public int Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态:0即将上线 1已上线
|
||||
/// 区域标识
|
||||
/// </summary>
|
||||
public int Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域名称(专业测评区域/更多区域)
|
||||
/// </summary>
|
||||
public string PositionName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 动作类型
|
||||
/// </summary>
|
||||
public int ActionType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 动作类型名称(跳转页面/弹客服二维码/即将上线)
|
||||
/// </summary>
|
||||
public string ActionTypeName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 状态:0=禁用,1=启用
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -16,4 +16,9 @@ public class HomeNavigationQueryRequest : PagedRequest
|
|||
/// 状态筛选
|
||||
/// </summary>
|
||||
public int? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域标识筛选
|
||||
/// </summary>
|
||||
public int? Position { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,17 @@ public class UpdateHomeNavigationRequest
|
|||
public int Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态:0即将上线 1已上线
|
||||
/// 区域标识:1=专业测评区域,2=更多区域
|
||||
/// </summary>
|
||||
public int Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 动作类型:1=跳转页面,2=弹客服二维码,3=即将上线
|
||||
/// </summary>
|
||||
public int ActionType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态:0=禁用,1=启用
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -564,13 +564,31 @@ public class ContentService : IContentService
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导航状态名称映射
|
||||
/// 导航状态名称映射:0=禁用,1=启用
|
||||
/// </summary>
|
||||
private static readonly Dictionary<int, string> NavigationStatusNames = new()
|
||||
{
|
||||
{ 0, "下线" },
|
||||
{ 1, "上线" },
|
||||
{ 2, "即将上线" }
|
||||
{ 0, "禁用" },
|
||||
{ 1, "启用" }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 导航区域标识名称映射
|
||||
/// </summary>
|
||||
private static readonly Dictionary<int, string> NavigationPositionNames = new()
|
||||
{
|
||||
{ 1, "专业测评区域" },
|
||||
{ 2, "更多区域" }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 导航动作类型名称映射
|
||||
/// </summary>
|
||||
private static readonly Dictionary<int, string> ActionTypeNames = new()
|
||||
{
|
||||
{ 1, "跳转页面" },
|
||||
{ 2, "弹客服二维码" },
|
||||
{ 3, "即将上线" }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -583,6 +601,26 @@ public class ContentService : IContentService
|
|||
return NavigationStatusNames.TryGetValue(status, out var name) ? name : "未知";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取导航区域名称
|
||||
/// </summary>
|
||||
/// <param name="position">区域标识</param>
|
||||
/// <returns>区域名称</returns>
|
||||
private static string GetNavigationPositionName(int position)
|
||||
{
|
||||
return NavigationPositionNames.TryGetValue(position, out var name) ? name : "未知";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取导航动作类型名称
|
||||
/// </summary>
|
||||
/// <param name="actionType">动作类型</param>
|
||||
/// <returns>动作类型名称</returns>
|
||||
private static string GetActionTypeName(int actionType)
|
||||
{
|
||||
return ActionTypeNames.TryGetValue(actionType, out var name) ? name : "未知";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 Position 值
|
||||
/// </summary>
|
||||
|
|
@ -616,6 +654,11 @@ public class ContentService : IContentService
|
|||
query = query.Where(n => n.Status == request.Status.Value);
|
||||
}
|
||||
|
||||
if (request.Position.HasValue)
|
||||
{
|
||||
query = query.Where(n => n.Position == request.Position.Value);
|
||||
}
|
||||
|
||||
var total = await query.CountAsync();
|
||||
|
||||
var items = await query
|
||||
|
|
@ -630,15 +673,19 @@ public class ContentService : IContentService
|
|||
ImageUrl = n.ImageUrl,
|
||||
LinkUrl = n.LinkUrl,
|
||||
Sort = n.Sort,
|
||||
Position = n.Position,
|
||||
ActionType = n.ActionType,
|
||||
Status = n.Status,
|
||||
CreateTime = n.CreateTime
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
// 在内存中映射状态名称(避免 EF Core LINQ 翻译问题)
|
||||
// 在内存中映射名称(避免 EF Core LINQ 翻译问题)
|
||||
foreach (var item in items)
|
||||
{
|
||||
item.StatusName = GetNavigationStatusName(item.Status);
|
||||
item.PositionName = GetNavigationPositionName(item.Position);
|
||||
item.ActionTypeName = GetActionTypeName(item.ActionType);
|
||||
}
|
||||
|
||||
return PagedResult<HomeNavigationDto>.Create(items, total, request.Page, request.PageSize);
|
||||
|
|
@ -657,6 +704,8 @@ public class ContentService : IContentService
|
|||
ImageUrl = n.ImageUrl,
|
||||
LinkUrl = n.LinkUrl,
|
||||
Sort = n.Sort,
|
||||
Position = n.Position,
|
||||
ActionType = n.ActionType,
|
||||
Status = n.Status,
|
||||
CreateTime = n.CreateTime
|
||||
})
|
||||
|
|
@ -667,8 +716,10 @@ public class ContentService : IContentService
|
|||
throw new BusinessException(ErrorCodes.NavigationNotFound, "首页导航不存在");
|
||||
}
|
||||
|
||||
// 在内存中映射状态名称
|
||||
// 在内存中映射名称
|
||||
nav.StatusName = GetNavigationStatusName(nav.Status);
|
||||
nav.PositionName = GetNavigationPositionName(nav.Position);
|
||||
nav.ActionTypeName = GetActionTypeName(nav.ActionType);
|
||||
|
||||
return nav;
|
||||
}
|
||||
|
|
@ -681,12 +732,20 @@ public class ContentService : IContentService
|
|||
throw new BusinessException(ErrorCodes.ParamError, "导航名称不能为空");
|
||||
}
|
||||
|
||||
// ActionType=1(跳转页面)时 LinkUrl 必填
|
||||
if (request.ActionType == 1 && string.IsNullOrWhiteSpace(request.LinkUrl))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ParamError, "跳转链接不能为空");
|
||||
}
|
||||
|
||||
var entity = new HomeNavigation
|
||||
{
|
||||
Name = request.Name,
|
||||
ImageUrl = request.ImageUrl,
|
||||
LinkUrl = request.LinkUrl,
|
||||
Sort = request.Sort,
|
||||
Position = request.Position,
|
||||
ActionType = request.ActionType,
|
||||
Status = request.Status,
|
||||
CreateTime = DateTime.Now,
|
||||
UpdateTime = DateTime.Now,
|
||||
|
|
@ -717,10 +776,18 @@ public class ContentService : IContentService
|
|||
throw new BusinessException(ErrorCodes.ParamError, "导航名称不能为空");
|
||||
}
|
||||
|
||||
// ActionType=1(跳转页面)时 LinkUrl 必填
|
||||
if (request.ActionType == 1 && string.IsNullOrWhiteSpace(request.LinkUrl))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ParamError, "跳转链接不能为空");
|
||||
}
|
||||
|
||||
entity.Name = request.Name;
|
||||
entity.ImageUrl = request.ImageUrl;
|
||||
entity.LinkUrl = request.LinkUrl;
|
||||
entity.Sort = request.Sort;
|
||||
entity.Position = request.Position;
|
||||
entity.ActionType = request.ActionType;
|
||||
entity.Status = request.Status;
|
||||
entity.UpdateTime = DateTime.Now;
|
||||
|
||||
|
|
|
|||
|
|
@ -308,10 +308,18 @@ export interface NavigationItem {
|
|||
linkUrl: string
|
||||
/** 排序值 */
|
||||
sort: number
|
||||
/** 状态 (0: 即将上线, 1: 已上线) */
|
||||
/** 状态 (0: 禁用, 1: 启用) */
|
||||
status: number
|
||||
/** 状态名称 */
|
||||
statusName: string
|
||||
/** 区域标识 (1: 专业测评区域, 2: 更多区域) */
|
||||
position: number
|
||||
/** 区域名称 */
|
||||
positionName: string
|
||||
/** 动作类型 (1: 跳转页面, 2: 弹客服二维码, 3: 即将上线) */
|
||||
actionType: number
|
||||
/** 动作类型名称 */
|
||||
actionTypeName: string
|
||||
/** 创建时间 */
|
||||
createTime: string
|
||||
}
|
||||
|
|
@ -324,6 +332,8 @@ export interface NavigationQuery extends PagedRequest {
|
|||
name?: string
|
||||
/** 状态筛选 */
|
||||
status?: number
|
||||
/** 区域标识筛选 */
|
||||
position?: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -340,6 +350,10 @@ export interface CreateNavigationRequest {
|
|||
sort: number
|
||||
/** 状态 */
|
||||
status: number
|
||||
/** 区域标识 (1: 专业测评区域, 2: 更多区域) */
|
||||
position: number
|
||||
/** 动作类型 (1: 跳转页面, 2: 弹客服二维码, 3: 即将上线) */
|
||||
actionType: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -29,9 +29,14 @@
|
|||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="上线" :value="1" />
|
||||
<el-option label="下线" :value="0" />
|
||||
<el-option label="即将上线" :value="2" />
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="区域">
|
||||
<el-select v-model="queryParams.position" placeholder="请选择区域" clearable>
|
||||
<el-option label="专业测评区域" :value="1" />
|
||||
<el-option label="更多区域" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
|
|
@ -74,6 +79,12 @@
|
|||
<!-- 名称 -->
|
||||
<el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip />
|
||||
|
||||
<!-- 区域 -->
|
||||
<el-table-column prop="positionName" label="区域" width="120" align="center" />
|
||||
|
||||
<!-- 动作类型 -->
|
||||
<el-table-column prop="actionTypeName" label="动作类型" width="120" align="center" />
|
||||
|
||||
<!-- 跳转链接 -->
|
||||
<el-table-column prop="linkUrl" label="跳转链接" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
|
|
@ -95,9 +106,8 @@
|
|||
</el-tag>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="1" :disabled="row.status === 1">上线</el-dropdown-item>
|
||||
<el-dropdown-item :command="0" :disabled="row.status === 0">下线</el-dropdown-item>
|
||||
<el-dropdown-item :command="2" :disabled="row.status === 2">即将上线</el-dropdown-item>
|
||||
<el-dropdown-item :command="1" :disabled="row.status === 1">启用</el-dropdown-item>
|
||||
<el-dropdown-item :command="0" :disabled="row.status === 0">禁用</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
|
@ -181,7 +191,22 @@
|
|||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="跳转链接" prop="linkUrl">
|
||||
<el-form-item label="区域" prop="position" required>
|
||||
<el-select v-model="state.formData.position" placeholder="请选择区域">
|
||||
<el-option label="专业测评区域" :value="1" />
|
||||
<el-option label="更多区域" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="动作类型" prop="actionType" required>
|
||||
<el-select v-model="state.formData.actionType" placeholder="请选择动作类型">
|
||||
<el-option label="跳转页面" :value="1" />
|
||||
<el-option label="弹客服二维码" :value="2" />
|
||||
<el-option label="即将上线" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="state.formData.actionType === 1" label="跳转链接" prop="linkUrl">
|
||||
<el-input
|
||||
v-model="state.formData.linkUrl"
|
||||
placeholder="请输入跳转链接,如:/pages/assessment/info/index"
|
||||
|
|
@ -200,9 +225,8 @@
|
|||
|
||||
<el-form-item label="状态" prop="status" required>
|
||||
<el-select v-model="state.formData.status" placeholder="请选择状态">
|
||||
<el-option label="上线" :value="1" />
|
||||
<el-option label="下线" :value="0" />
|
||||
<el-option label="即将上线" :value="2" />
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
|
@ -246,6 +270,8 @@ interface NavigationFormData {
|
|||
imageUrl: string
|
||||
linkUrl: string
|
||||
sort: number
|
||||
position: number
|
||||
actionType: number
|
||||
status: number
|
||||
}
|
||||
|
||||
|
|
@ -290,6 +316,15 @@ const formRules: FormRules = {
|
|||
name: [
|
||||
{ required: true, message: '请输入导航名称', trigger: 'blur' }
|
||||
],
|
||||
position: [
|
||||
{ required: true, message: '请选择区域', trigger: 'change' }
|
||||
],
|
||||
actionType: [
|
||||
{ required: true, message: '请选择动作类型', trigger: 'change' }
|
||||
],
|
||||
linkUrl: [
|
||||
{ required: true, message: '请输入跳转链接', trigger: 'blur' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
]
|
||||
|
|
@ -303,19 +338,21 @@ function getDefaultFormData(): NavigationFormData {
|
|||
imageUrl: '',
|
||||
linkUrl: '',
|
||||
sort: 0,
|
||||
position: 1,
|
||||
actionType: 1,
|
||||
status: 1
|
||||
}
|
||||
}
|
||||
|
||||
/** 状态文本映射 */
|
||||
function statusLabel(status: number): string {
|
||||
const map: Record<number, string> = { 0: '下线', 1: '上线', 2: '即将上线' }
|
||||
const map: Record<number, string> = { 0: '禁用', 1: '启用' }
|
||||
return map[status] ?? '未知'
|
||||
}
|
||||
|
||||
/** 状态标签颜色映射 */
|
||||
function statusTagType(status: number): '' | 'success' | 'info' | 'warning' | 'danger' {
|
||||
const map: Record<number, '' | 'success' | 'info' | 'warning' | 'danger'> = { 0: 'info', 1: 'success', 2: 'warning' }
|
||||
const map: Record<number, '' | 'success' | 'info' | 'warning' | 'danger'> = { 0: 'danger', 1: 'success' }
|
||||
return map[status] ?? 'info'
|
||||
}
|
||||
|
||||
|
|
@ -332,6 +369,9 @@ async function loadList() {
|
|||
if (queryParams.status !== undefined && queryParams.status !== '') {
|
||||
params.status = Number(queryParams.status)
|
||||
}
|
||||
if (queryParams.position !== undefined && queryParams.position !== '') {
|
||||
params.position = Number(queryParams.position)
|
||||
}
|
||||
|
||||
const res = await getNavigationList(params)
|
||||
if (res.code === 0) {
|
||||
|
|
@ -358,6 +398,7 @@ function handleSearch() {
|
|||
function handleReset() {
|
||||
queryParams.name = ''
|
||||
queryParams.status = undefined
|
||||
queryParams.position = undefined
|
||||
queryParams.page = 1
|
||||
loadList()
|
||||
}
|
||||
|
|
@ -389,6 +430,8 @@ function handleEdit(row: NavigationItem) {
|
|||
imageUrl: row.imageUrl || '',
|
||||
linkUrl: row.linkUrl || '',
|
||||
sort: row.sort,
|
||||
position: row.position,
|
||||
actionType: row.actionType,
|
||||
status: row.status
|
||||
}
|
||||
state.dialogVisible = true
|
||||
|
|
@ -446,8 +489,10 @@ async function handleSubmit() {
|
|||
const requestData: CreateNavigationRequest | UpdateNavigationRequest = {
|
||||
name: formData.name,
|
||||
imageUrl: formData.imageUrl || undefined,
|
||||
linkUrl: formData.linkUrl || undefined,
|
||||
linkUrl: formData.actionType === 1 ? (formData.linkUrl || undefined) : undefined,
|
||||
sort: formData.sort,
|
||||
position: formData.position,
|
||||
actionType: formData.actionType,
|
||||
status: formData.status
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,19 +89,22 @@ public class HomeController : ControllerBase
|
|||
/// 获取首页导航入口列表
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// GET /api/home/getNavigationList
|
||||
/// GET /api/home/getNavigationList?position=1
|
||||
///
|
||||
/// 返回所有启用状态的首页导航入口,按Sort降序排列
|
||||
/// 返回启用状态的首页导航入口,按Sort降序排列
|
||||
/// 支持按 position 筛选区域(1=专业测评,2=更多),不传返回全部
|
||||
/// 不需要用户登录认证
|
||||
/// Requirements: 2.1, 2.2
|
||||
/// </remarks>
|
||||
/// <param name="position">区域标识(可选):1=专业测评区域,2=更多区域</param>
|
||||
/// <returns>导航入口列表</returns>
|
||||
[HttpGet("getNavigationList")]
|
||||
[ProducesResponseType(typeof(ApiResponse<List<HomeNavigationDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<List<HomeNavigationDto>>> GetNavigationList()
|
||||
public async Task<ApiResponse<List<HomeNavigationDto>>> GetNavigationList([FromQuery] int? position)
|
||||
{
|
||||
try
|
||||
{
|
||||
var navigations = await _homeService.GetNavigationListAsync();
|
||||
var navigations = await _homeService.GetNavigationListAsync(position);
|
||||
return ApiResponse<List<HomeNavigationDto>>.Success(navigations);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -29,11 +29,8 @@ public interface IHomeService
|
|||
/// <summary>
|
||||
/// 获取首页导航入口列表
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 返回所有启用状态的首页导航入口,按Sort降序排列
|
||||
/// </remarks>
|
||||
/// <returns>导航入口列表</returns>
|
||||
Task<List<HomeNavigationDto>> GetNavigationListAsync();
|
||||
/// <param name="position">区域标识(可选),不传返回全部</param>
|
||||
Task<List<HomeNavigationDto>> GetNavigationListAsync(int? position = null);
|
||||
|
||||
/// <summary>
|
||||
/// 获取宣传图列表
|
||||
|
|
|
|||
|
|
@ -76,13 +76,20 @@ public class HomeService : IHomeService
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<HomeNavigationDto>> GetNavigationListAsync()
|
||||
public async Task<List<HomeNavigationDto>> GetNavigationListAsync(int? position = null)
|
||||
{
|
||||
_logger.LogDebug("获取首页导航入口列表");
|
||||
_logger.LogDebug("获取首页导航入口列表, position={Position}", position);
|
||||
|
||||
var navigations = await _dbContext.HomeNavigations
|
||||
var query = _dbContext.HomeNavigations
|
||||
.AsNoTracking()
|
||||
.Where(n => n.Status != 0 && !n.IsDeleted)
|
||||
.Where(n => !n.IsDeleted && n.Status == 1);
|
||||
|
||||
if (position.HasValue)
|
||||
{
|
||||
query = query.Where(n => n.Position == position.Value);
|
||||
}
|
||||
|
||||
var navigations = await query
|
||||
.OrderByDescending(n => n.Sort)
|
||||
.Select(n => new HomeNavigationDto
|
||||
{
|
||||
|
|
@ -90,7 +97,9 @@ public class HomeService : IHomeService
|
|||
Name = n.Name,
|
||||
ImageUrl = n.ImageUrl ?? string.Empty,
|
||||
LinkUrl = n.LinkUrl,
|
||||
Status = n.Status
|
||||
Status = n.Status,
|
||||
Position = n.Position,
|
||||
ActionType = n.ActionType
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,17 @@ public class HomeNavigation
|
|||
public int Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态:0即将上线 1已上线
|
||||
/// 区域标识:1=专业测评区域,2=更多区域
|
||||
/// </summary>
|
||||
public int Position { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 动作类型:1=跳转页面,2=弹客服二维码,3=即将上线
|
||||
/// </summary>
|
||||
public int ActionType { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 状态:0=禁用,1=启用
|
||||
/// </summary>
|
||||
public int Status { get; set; } = 1;
|
||||
|
||||
|
|
|
|||
|
|
@ -29,4 +29,14 @@ public class HomeNavigationDto
|
|||
/// 状态:0即将上线 1已上线
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域标识
|
||||
/// </summary>
|
||||
public int Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 动作类型
|
||||
/// </summary>
|
||||
public int ActionType { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,12 @@ export function getAssessmentList() {
|
|||
|
||||
/**
|
||||
* 获取首页导航入口列表
|
||||
* @param {Object} [params] - 查询参数
|
||||
* @param {number} [params.position] - 区域标识
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export function getNavigationList() {
|
||||
return get('/home/getNavigationList')
|
||||
export function getNavigationList(params) {
|
||||
return get('/home/getNavigationList', params)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
</view>
|
||||
|
||||
<!-- 专业测评入口 -->
|
||||
<view class="section-card" v-if="navigationList.length > 0">
|
||||
<view class="section-card" v-if="assessmentList.length > 0">
|
||||
<view class="section-header">
|
||||
<view class="section-indicator"></view>
|
||||
<text class="section-title">专业测评</text>
|
||||
|
|
@ -52,21 +52,19 @@
|
|||
<view class="assessment-grid">
|
||||
<view
|
||||
class="assessment-card"
|
||||
v-for="(item, index) in navigationList"
|
||||
v-for="(item, index) in assessmentList"
|
||||
:key="index"
|
||||
:class="'assessment-card--' + index"
|
||||
@click="handleNavigationClick(item)"
|
||||
@click="handleCardClick(item)"
|
||||
>
|
||||
<!-- 即将上线标签 -->
|
||||
<view v-if="item.status === 2" class="coming-soon-tag">
|
||||
<view v-if="item.actionType === 3" class="coming-soon-tag">
|
||||
<text>即将上线</text>
|
||||
</view>
|
||||
<image
|
||||
:src="item.imageUrl"
|
||||
mode="aspectFit"
|
||||
class="assessment-icon"
|
||||
mode="aspectFill"
|
||||
class="assessment-image"
|
||||
/>
|
||||
<text class="assessment-name">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -83,24 +81,22 @@
|
|||
</view>
|
||||
|
||||
<!-- 更多 -->
|
||||
<view class="section-card">
|
||||
<view class="section-card" v-if="moreList.length > 0">
|
||||
<view class="section-header">
|
||||
<view class="section-indicator"></view>
|
||||
<text class="section-title">更多</text>
|
||||
</view>
|
||||
<view class="more-grid">
|
||||
<view class="more-card more-card--plan" @click="goToStudyPlan">
|
||||
<text class="more-card__title">学习方案</text>
|
||||
<view
|
||||
class="more-card"
|
||||
v-for="(item, index) in moreList"
|
||||
:key="index"
|
||||
:class="{ 'more-card--full': moreList.length % 2 === 1 && index === moreList.length - 1 }"
|
||||
@click="handleCardClick(item)"
|
||||
>
|
||||
<text class="more-card__title">{{ item.name }}</text>
|
||||
<image
|
||||
src="/static/icons/study-plan.png"
|
||||
mode="aspectFit"
|
||||
class="more-card__icon"
|
||||
/>
|
||||
</view>
|
||||
<view class="more-card more-card--consult" @click="goToConsult">
|
||||
<text class="more-card__title">详细咨询</text>
|
||||
<image
|
||||
src="/static/icons/consult.png"
|
||||
:src="item.imageUrl"
|
||||
mode="aspectFit"
|
||||
class="more-card__icon"
|
||||
/>
|
||||
|
|
@ -116,6 +112,15 @@
|
|||
<!-- 底部安全间距 -->
|
||||
<view class="safe-bottom"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 客服二维码弹窗 -->
|
||||
<Popup
|
||||
:visible="showQrPopup"
|
||||
:imageUrl="qrcodeUrl"
|
||||
title="扫码咨询"
|
||||
:content="qrcodeUrl ? '' : '暂未配置客服二维码'"
|
||||
@close="showQrPopup = false"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
|
@ -124,7 +129,9 @@ import { ref, onMounted } from 'vue'
|
|||
import { useUserStore } from '@/store/user.js'
|
||||
import { useNavbar } from '@/composables/useNavbar.js'
|
||||
import { getBannerList, getNavigationList } from '@/api/home.js'
|
||||
import { getContactInfo } from '@/api/system.js'
|
||||
import Loading from '@/components/Loading/index.vue'
|
||||
import Popup from '@/components/Popup/index.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { statusBarHeight, navbarHeight, totalNavbarHeight } = useNavbar()
|
||||
|
|
@ -133,7 +140,10 @@ const { statusBarHeight, navbarHeight, totalNavbarHeight } = useNavbar()
|
|||
const pageLoading = ref(true)
|
||||
const isRefreshing = ref(false)
|
||||
const bannerList = ref([])
|
||||
const navigationList = ref([])
|
||||
const assessmentList = ref([])
|
||||
const moreList = ref([])
|
||||
const qrcodeUrl = ref('')
|
||||
const showQrPopup = ref(false)
|
||||
|
||||
/**
|
||||
* 加载Banner数据
|
||||
|
|
@ -150,16 +160,44 @@ async function loadBannerList() {
|
|||
}
|
||||
|
||||
/**
|
||||
* 加载首页导航入口数据
|
||||
* 加载专业测评区域数据(position=1)
|
||||
*/
|
||||
async function loadNavigationList() {
|
||||
async function loadAssessmentList() {
|
||||
try {
|
||||
const res = await getNavigationList()
|
||||
const res = await getNavigationList({ position: 1 })
|
||||
if (res && res.code === 0 && res.data) {
|
||||
navigationList.value = Array.isArray(res.data) ? res.data : (res.data.list || [])
|
||||
assessmentList.value = Array.isArray(res.data) ? res.data : (res.data.list || [])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载导航入口失败:', error)
|
||||
console.error('加载专业测评数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载更多区域数据(position=2)
|
||||
*/
|
||||
async function loadMoreList() {
|
||||
try {
|
||||
const res = await getNavigationList({ position: 2 })
|
||||
if (res && res.code === 0 && res.data) {
|
||||
moreList.value = Array.isArray(res.data) ? res.data : (res.data.list || [])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载更多区域数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载客服二维码URL
|
||||
*/
|
||||
async function loadContactInfo() {
|
||||
try {
|
||||
const res = await getContactInfo()
|
||||
if (res && res.code === 0 && res.data) {
|
||||
qrcodeUrl.value = res.data.qrcodeUrl || ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载客服二维码失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +210,9 @@ async function initPageData() {
|
|||
userStore.restoreFromStorage()
|
||||
await Promise.all([
|
||||
loadBannerList(),
|
||||
loadNavigationList()
|
||||
loadAssessmentList(),
|
||||
loadMoreList(),
|
||||
loadContactInfo()
|
||||
])
|
||||
} finally {
|
||||
pageLoading.value = false
|
||||
|
|
@ -199,13 +239,18 @@ function handleBannerClick(item) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 处理导航入口点击
|
||||
* 处理卡片点击(根据 ActionType 分发)
|
||||
*/
|
||||
function handleNavigationClick(item) {
|
||||
if (item.status === 2) {
|
||||
uni.showToast({ title: '该功能即将上线', icon: 'none', duration: 2000 })
|
||||
function handleCardClick(item) {
|
||||
if (item.actionType === 3) {
|
||||
uni.showToast({ title: '即将上线', icon: 'none', duration: 2000 })
|
||||
return
|
||||
}
|
||||
if (item.actionType === 2) {
|
||||
showQrPopup.value = true
|
||||
return
|
||||
}
|
||||
// actionType === 1: 跳转页面
|
||||
if (!item.linkUrl) return
|
||||
uni.navigateTo({
|
||||
url: item.linkUrl,
|
||||
|
|
@ -222,20 +267,6 @@ function goToPlanner() {
|
|||
uni.navigateTo({ url: '/pages/planner/list/index' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转学习方案(业务详情页)
|
||||
*/
|
||||
function goToStudyPlan() {
|
||||
uni.navigateTo({ url: '/pages/business/detail/index?type=study-plan' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转详细咨询(业务详情页)
|
||||
*/
|
||||
function goToConsult() {
|
||||
uni.navigateTo({ url: '/pages/business/detail/index?type=consult' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 下拉刷新
|
||||
*/
|
||||
|
|
@ -349,24 +380,10 @@ onMounted(() => {
|
|||
.assessment-card {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 240rpx;
|
||||
border-radius: $border-radius-xl;
|
||||
overflow: hidden;
|
||||
|
||||
// 第一个卡片 - 红/橙渐变
|
||||
&--0 {
|
||||
background: linear-gradient(135deg, #FF7B6B, #FF5B5B);
|
||||
}
|
||||
|
||||
// 第二个卡片 - 橙/黄渐变
|
||||
&--1 {
|
||||
background: linear-gradient(135deg, #FFD080, #FFB347);
|
||||
}
|
||||
|
||||
.coming-soon-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -382,16 +399,9 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
.assessment-icon {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.assessment-name {
|
||||
font-size: $font-size-md;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-white;
|
||||
.assessment-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -412,14 +422,15 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
// 更多 - 两列卡片
|
||||
// 更多 - 网格布局,每行2个
|
||||
.more-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.more-card {
|
||||
flex: 1;
|
||||
width: calc(50% - #{$spacing-md} / 2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
|
@ -427,13 +438,12 @@ onMounted(() => {
|
|||
padding: 0 $spacing-lg;
|
||||
border-radius: $border-radius-xl;
|
||||
overflow: hidden;
|
||||
background-color: $bg-gray;
|
||||
box-sizing: border-box;
|
||||
|
||||
&--plan {
|
||||
background: linear-gradient(135deg, #FFF5EB, #FFE8D5);
|
||||
}
|
||||
|
||||
&--consult {
|
||||
background: linear-gradient(135deg, #FFE8EC, #FFD5DC);
|
||||
// 奇数个时最后一个占满整行
|
||||
&--full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__title {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user