This commit is contained in:
zpc 2026-03-19 08:11:34 +08:00
parent da590c2939
commit 15357e0fec
13 changed files with 333 additions and 38 deletions

View File

@ -0,0 +1,29 @@
-- =============================================
-- Business Pages Table - Add Icon Fields
-- 学业邑规划 - MiAssessment
--
-- 新增团队页 icon 配置字段:
-- IconUrl - 未选中状态图标
-- ActiveIconUrl - 选中状态图标
--
-- Database: SQL Server 2022
-- =============================================
USE [MiAssessment_Business];
GO
-- 新增 IconUrl 字段
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[business_pages]') AND name = 'IconUrl')
BEGIN
ALTER TABLE [dbo].[business_pages] ADD [IconUrl] NVARCHAR(500) NULL;
PRINT N'Column IconUrl added successfully';
END
GO
-- 新增 ActiveIconUrl 字段
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[business_pages]') AND name = 'ActiveIconUrl')
BEGIN
ALTER TABLE [dbo].[business_pages] ADD [ActiveIconUrl] NVARCHAR(500) NULL;
PRINT N'Column ActiveIconUrl added successfully';
END
GO

View File

@ -24,7 +24,19 @@ public class BusinessPage
public string Title { get; set; } = null!;
/// <summary>
/// 图片URL
/// 图标URL未选中状态
/// </summary>
[MaxLength(500)]
public string? IconUrl { get; set; }
/// <summary>
/// 图标URL选中状态
/// </summary>
[MaxLength(500)]
public string? ActiveIconUrl { get; set; }
/// <summary>
/// 详情图片URL
/// </summary>
[Required]
[MaxLength(500)]

View File

@ -16,7 +16,17 @@ public class BusinessPageDto
public string Title { get; set; } = null!;
/// <summary>
/// 图片URL长图
/// 图标URL未选中状态
/// </summary>
public string? IconUrl { get; set; }
/// <summary>
/// 图标URL选中状态
/// </summary>
public string? ActiveIconUrl { get; set; }
/// <summary>
/// 详情图片URL长图
/// </summary>
public string ImageUrl { get; set; } = null!;

View File

@ -15,7 +15,19 @@ public class CreateBusinessPageRequest
public string Title { get; set; } = null!;
/// <summary>
/// 图片URL必填长图
/// 图标URL未选中状态
/// </summary>
[MaxLength(500, ErrorMessage = "图标URL长度不能超过500个字符")]
public string? IconUrl { get; set; }
/// <summary>
/// 图标URL选中状态
/// </summary>
[MaxLength(500, ErrorMessage = "选中图标URL长度不能超过500个字符")]
public string? ActiveIconUrl { get; set; }
/// <summary>
/// 详情图片URL必填长图
/// </summary>
[Required(ErrorMessage = "图片URL不能为空")]
[MaxLength(500, ErrorMessage = "图片URL长度不能超过500个字符")]

View File

@ -21,7 +21,19 @@ public class UpdateBusinessPageRequest
public string Title { get; set; } = null!;
/// <summary>
/// 图片URL必填长图
/// 图标URL未选中状态
/// </summary>
[MaxLength(500, ErrorMessage = "图标URL长度不能超过500个字符")]
public string? IconUrl { get; set; }
/// <summary>
/// 图标URL选中状态
/// </summary>
[MaxLength(500, ErrorMessage = "选中图标URL长度不能超过500个字符")]
public string? ActiveIconUrl { get; set; }
/// <summary>
/// 详情图片URL必填长图
/// </summary>
[Required(ErrorMessage = "图片URL不能为空")]
[MaxLength(500, ErrorMessage = "图片URL长度不能超过500个字符")]

View File

@ -63,6 +63,8 @@ public class BusinessPageService : IBusinessPageService
{
Id = p.Id,
Title = p.Title,
IconUrl = p.IconUrl,
ActiveIconUrl = p.ActiveIconUrl,
ImageUrl = p.ImageUrl,
HasActionButton = p.HasActionButton,
ActionButtonText = p.ActionButtonText,
@ -90,6 +92,8 @@ public class BusinessPageService : IBusinessPageService
{
Id = p.Id,
Title = p.Title,
IconUrl = p.IconUrl,
ActiveIconUrl = p.ActiveIconUrl,
ImageUrl = p.ImageUrl,
HasActionButton = p.HasActionButton,
ActionButtonText = p.ActionButtonText,
@ -126,6 +130,8 @@ public class BusinessPageService : IBusinessPageService
var entity = new BusinessPage
{
Title = request.Title,
IconUrl = request.IconUrl,
ActiveIconUrl = request.ActiveIconUrl,
ImageUrl = request.ImageUrl,
HasActionButton = request.HasActionButton,
ActionButtonText = request.ActionButtonText,
@ -166,6 +172,8 @@ public class BusinessPageService : IBusinessPageService
// 更新字段
entity.Title = request.Title;
entity.IconUrl = request.IconUrl;
entity.ActiveIconUrl = request.ActiveIconUrl;
entity.ImageUrl = request.ImageUrl;
entity.HasActionButton = request.HasActionButton;
entity.ActionButtonText = request.ActionButtonText;

View File

@ -12,6 +12,8 @@ import type { PagedRequest, PagedResult, UpdateStatusRequest } from '@/types/com
export interface BusinessPageItem {
id: number
title: string
iconUrl?: string
activeIconUrl?: string
imageUrl: string
hasActionButton: boolean
actionButtonText?: string
@ -31,6 +33,8 @@ export interface BusinessPageQuery extends PagedRequest {
/** 创建请求 */
export interface CreateBusinessPageRequest {
title: string
iconUrl?: string
activeIconUrl?: string
imageUrl: string
hasActionButton: boolean
actionButtonText?: string

View File

@ -150,6 +150,24 @@
<el-input v-model="state.formData.title" placeholder="请输入标题" maxlength="100" show-word-limit />
</el-form-item>
<el-form-item label="图标(未选中)">
<ImageUpload
v-model="state.formData.iconUrl"
placeholder="点击上传未选中图标"
tip="建议尺寸64x64pxpng格式"
:max-size="2"
/>
</el-form-item>
<el-form-item label="图标(选中)">
<ImageUpload
v-model="state.formData.activeIconUrl"
placeholder="点击上传选中图标"
tip="建议尺寸64x64pxpng格式"
:max-size="2"
/>
</el-form-item>
<el-form-item label="介绍图片" prop="imageUrl" required>
<ImageUpload
v-model="state.formData.imageUrl"
@ -215,6 +233,8 @@ import { DictSelect, ImageUpload } from '@/components'
interface FormData {
id?: number
title: string
iconUrl: string
activeIconUrl: string
imageUrl: string
hasActionButton: boolean
actionButtonText: string
@ -269,6 +289,8 @@ const formRules: FormRules = {
function getDefaultFormData(): FormData {
return {
title: '',
iconUrl: '',
activeIconUrl: '',
imageUrl: '',
hasActionButton: false,
actionButtonText: '',
@ -325,6 +347,8 @@ function handleEdit(row: BusinessPageItem) {
state.formData = {
id: row.id,
title: row.title || '',
iconUrl: row.iconUrl || '',
activeIconUrl: row.activeIconUrl || '',
imageUrl: row.imageUrl,
hasActionButton: row.hasActionButton,
actionButtonText: row.actionButtonText || '',
@ -376,6 +400,8 @@ async function handleSubmit() {
const fd = state.formData
const data: CreateBusinessPageRequest | UpdateBusinessPageRequest = {
title: fd.title,
iconUrl: fd.iconUrl || undefined,
activeIconUrl: fd.activeIconUrl || undefined,
imageUrl: fd.imageUrl,
hasActionButton: fd.hasActionButton,
actionButtonText: fd.hasActionButton ? fd.actionButtonText : undefined,

View File

@ -32,18 +32,28 @@ public class TeamService : ITeamService
{
_logger.LogDebug("获取团队介绍信息");
var images = await _dbContext.Promotions
// 从 business_pages 表查询启用状态的业务页,按排序降序
var pages = await _dbContext.BusinessPages
.AsNoTracking()
.Where(p => p.Position == 2 && p.Status == 1 && !p.IsDeleted)
.Where(p => p.Status == 1 && !p.IsDeleted)
.OrderByDescending(p => p.Sort)
.Select(p => p.ImageUrl)
.Select(p => new TeamPageItemDto
{
Id = p.Id,
Title = p.Title ?? "",
IconUrl = p.IconUrl,
ActiveIconUrl = p.ActiveIconUrl,
ImageUrl = p.ImageUrl
})
.ToListAsync();
_logger.LogDebug("获取到 {Count} 张团队介绍图片", images.Count);
_logger.LogDebug("获取到 {Count} 个团队业务页", pages.Count);
return new TeamInfoDto
{
Images = images
// 兼容旧版:保留 images 字段
Images = pages.Select(p => p.ImageUrl).ToList(),
Items = pages
};
}
}

View File

@ -422,9 +422,15 @@ public partial class MiAssessmentDbContext : DbContext
entity.Property(e => e.Title)
.HasMaxLength(100)
.HasComment("标题");
entity.Property(e => e.IconUrl)
.HasMaxLength(500)
.HasComment("图标URL未选中状态");
entity.Property(e => e.ActiveIconUrl)
.HasMaxLength(500)
.HasComment("图标URL选中状态");
entity.Property(e => e.ImageUrl)
.HasMaxLength(500)
.HasComment("图片URL");
.HasComment("详情图片URL");
entity.Property(e => e.ShowButton)
.HasComment("是否显示操作按钮");
entity.Property(e => e.ButtonText)

View File

@ -23,7 +23,19 @@ public class BusinessPage
public string? Title { get; set; }
/// <summary>
/// 图片URL
/// 图标URL未选中状态
/// </summary>
[MaxLength(500)]
public string? IconUrl { get; set; }
/// <summary>
/// 图标URL选中状态
/// </summary>
[MaxLength(500)]
public string? ActiveIconUrl { get; set; }
/// <summary>
/// 详情图片URL
/// </summary>
[Required]
[MaxLength(500)]

View File

@ -6,7 +6,43 @@ namespace MiAssessment.Model.Models.Team;
public class TeamInfoDto
{
/// <summary>
/// 团队介绍图片列表
/// 团队介绍图片列表(兼容旧版)
/// </summary>
public List<string> Images { get; set; } = new();
/// <summary>
/// 团队业务页列表含标题、icon、详情图
/// </summary>
public List<TeamPageItemDto> Items { get; set; } = new();
}
/// <summary>
/// 团队页单项 DTO
/// </summary>
public class TeamPageItemDto
{
/// <summary>
/// 主键ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; } = null!;
/// <summary>
/// 图标URL未选中状态
/// </summary>
public string? IconUrl { get; set; }
/// <summary>
/// 图标URL选中状态
/// </summary>
public string? ActiveIconUrl { get; set; }
/// <summary>
/// 详情图片URL
/// </summary>
public string ImageUrl { get; set; } = null!;
}

View File

@ -3,16 +3,57 @@
<!-- 加载状态 -->
<Loading v-if="pageLoading" type="page" :loading="true" />
<!-- 团队介绍图片 -->
<view v-else class="team-content">
<template v-if="teamImages.length > 0">
<image
v-for="(item, index) in teamImages"
:key="index"
:src="item"
mode="scaleToFill"
class="team-image"
/>
<template v-else>
<!-- 有数据 -->
<template v-if="items.length > 0">
<!-- 自定义导航栏 -->
<Navbar title="团队" :show-back="false" background-color="#F5A623" text-color="light" />
<!-- 顶部 tab 可横向滚动 -->
<scroll-view
scroll-x
:scroll-left="scrollLeft"
class="tab-scroll"
:show-scrollbar="false"
>
<view class="tab-list">
<view
v-for="(item, index) in items"
:key="item.id"
class="tab-item"
:class="{ 'tab-item--active': activeIndex === index }"
@click="handleTabClick(index)"
>
<!-- icon 区域 -->
<view class="tab-icon-wrap">
<!-- icon -->
<image
v-if="activeIndex === index && item.activeIconUrl"
:src="item.activeIconUrl"
mode="aspectFit"
class="tab-icon"
/>
<image
v-else-if="item.iconUrl"
:src="item.iconUrl"
mode="aspectFit"
class="tab-icon"
/>
</view>
<!-- 标题 -->
<text class="tab-title">{{ item.title }}</text>
</view>
</view>
</scroll-view>
<!-- 详情图片 -->
<view class="detail-content">
<image
:src="currentItem.imageUrl"
mode="widthFix"
class="detail-image"
/>
</view>
</template>
<!-- 无数据时显示空状态 -->
@ -20,22 +61,38 @@
<image src="/static/ic_empty.png" mode="aspectFit" class="empty-icon" />
<text class="empty-text">暂无团队介绍</text>
</view>
</view>
</template>
</view>
</template>
<script setup>
/**
* 团队页面
* 展示团队介绍图片全屏无导航栏
* 顶部可配置 tab 标题+icon点击切换对应详情图片
* tab 超出屏幕时可横向滚动
*/
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { getTeamInfo } from '@/api/team.js'
import Loading from '@/components/Loading/index.vue'
import Navbar from '@/components/Navbar/index.vue'
//
const pageLoading = ref(true)
const teamImages = ref([])
const items = ref([])
const activeIndex = ref(0)
const scrollLeft = ref(0)
//
const currentItem = computed(() => {
return items.value[activeIndex.value] || {}
})
/**
* 切换 tab
*/
function handleTabClick(index) {
activeIndex.value = index
}
/**
* 加载团队介绍数据
@ -45,13 +102,18 @@ async function loadTeamInfo() {
try {
const res = await getTeamInfo()
if (res && res.code === 0 && res.data) {
const images = res.data.images || res.data.list || res.data
if (Array.isArray(images)) {
teamImages.value = images.map(item =>
typeof item === 'string' ? item : (item.imageUrl || item)
)
} else {
teamImages.value = []
// 使 items
if (res.data.items && res.data.items.length > 0) {
items.value = res.data.items
} else if (res.data.images && res.data.images.length > 0) {
//
items.value = res.data.images.map((url, index) => ({
id: index,
title: `介绍${index + 1}`,
iconUrl: '',
activeIconUrl: '',
imageUrl: typeof url === 'string' ? url : (url.imageUrl || '')
}))
}
}
} catch (error) {
@ -75,14 +137,70 @@ onMounted(() => {
background-color: $bg-white;
}
.team-content {
.team-image {
width: 100vw;
height: 100vh;
display: block;
// tab
.tab-scroll {
white-space: nowrap;
background-color: $bg-white;
padding: $spacing-lg 0;
}
.tab-list {
display: inline-flex;
padding: 0 $spacing-lg;
gap: $spacing-xl;
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
width: 140rpx;
}
.tab-icon-wrap {
position: relative;
width: 100rpx;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: $spacing-xs;
}
.tab-icon {
width: 64rpx;
height: 64rpx;
}
.tab-title {
font-size: $font-size-sm;
color: $text-secondary;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 140rpx;
}
.tab-item--active {
.tab-title {
color: $text-color;
font-weight: $font-weight-medium;
}
}
//
.detail-content {
width: 100%;
}
.detail-image {
width: 100%;
display: block;
}
//
.empty-state {
display: flex;
flex-direction: column;