This commit is contained in:
zpc 2026-03-18 11:53:46 +08:00
parent 86584798a3
commit 2f75cc611b
21 changed files with 277 additions and 25 deletions

View File

@ -1249,7 +1249,7 @@ interface ConfigPageState {
| order_type | 订单类型 | 1-测评订单, 2-规划订单 |
| order_status | 订单状态 | 1-待支付, 2-已支付, 3-已完成, 4-退款中, 5-已退款, 6-已取消 |
| pay_type | 支付方式 | 1-微信支付 |
| booking_status | 预约状态 | 1-待确认, 2-已确认, 3-已完成, 4-已取消 |
| booking_status | 预约状态 | 1-待联系, 2-已联系, 3-已完成, 4-已取消 |
| invite_code_status | 邀请码状态 | 1-未分配, 2-已分配, 3-已使用 |
| commission_level | 佣金层级 | 1-直接下级, 2-间接下级 |
| commission_status | 佣金状态 | 1-待结算, 2-已结算 |

View File

@ -229,7 +229,7 @@
- 实现搜索表单(规划师、用户、日期范围、状态)
- 实现分页表格预约ID、用户信息、规划师信息、预约日期、学生信息、状态、创建时间、操作
- 实现预约详情抽屉(完整预约信息、用户详情、规划师详情、学生信息)
- 实现状态修改对话框(待确认/已确认/已完成/已取消)
- 实现状态修改对话框(待联系/已联系/已完成/已取消)
- 实现导出功能
- 使用 DictSelect 组件选择状态
- _Requirements: 13.1, 13.2, 13.3, 13.4, 13.5, 13.6_

View File

@ -368,7 +368,7 @@
| ScoreBiology | int | 否 | - | 生物成绩 |
| ScoreGeography | int | 否 | - | 地理成绩 |
| ScorePolitics | int | 否 | - | 政治成绩 |
| Status | int | 是 | 1 | 状态1待确认 2已确认 3已完成 4已取消 |
| Status | int | 是 | 1 | 状态1待联系 2已联系 3已完成 4已取消 |
| CreateTime | datetime2 | 是 | GETDATE() | 创建时间 |
| UpdateTime | datetime2 | 是 | GETDATE() | 更新时间 |
| IsDeleted | bit | 是 | 0 | 软删除 |

View File

@ -472,7 +472,7 @@ BEGIN
[ScoreBiology] INT NULL, -- 生物成绩
[ScoreGeography] INT NULL, -- 地理成绩
[ScorePolitics] INT NULL, -- 政治成绩
[Status] INT NOT NULL DEFAULT 1, -- 状态1待确认 2已确认 3已完成 4已取消
[Status] INT NOT NULL DEFAULT 1, -- 状态1待联系 2已联系 3已完成 4已取消
[CreateTime] DATETIME2 NOT NULL DEFAULT GETDATE(),
[UpdateTime] DATETIME2 NOT NULL DEFAULT GETDATE(),
[IsDeleted] BIT NOT NULL DEFAULT 0,

View File

@ -127,7 +127,7 @@ public class PlannerBooking
public string? Expectation { get; set; }
/// <summary>
/// 状态1待确认 2已确认 3已完成 4已取消
/// 状态1待联系 2已联系 3已完成 4已取消
/// </summary>
public int Status { get; set; } = 1;

View File

@ -86,7 +86,7 @@ public class BookingDto
public string GradeName { get; set; } = null!;
/// <summary>
/// 状态1待确认 2已确认 3已完成 4已取消
/// 状态1待联系 2已联系 3已完成 4已取消
/// </summary>
public int Status { get; set; }

View File

@ -26,7 +26,7 @@ public class BookingQueryRequest : PagedRequest
public DateTime? BookingDateEnd { get; set; }
/// <summary>
/// 状态1待确认 2已确认 3已完成 4已取消
/// 状态1待联系 2已联系 3已完成 4已取消
/// </summary>
public int? Status { get; set; }

View File

@ -14,7 +14,7 @@ public class UpdateBookingStatusRequest
public long Id { get; set; }
/// <summary>
/// 状态1待确认 2已确认 3已完成 4已取消
/// 状态1待联系 2已联系 3已完成 4已取消
/// </summary>
[Range(1, 4, ErrorMessage = "状态值无效")]
public int Status { get; set; }

View File

@ -329,8 +329,8 @@ public class AssessmentRecordService : IAssessmentRecordService
return (parent.Id, parent.Name);
}
// 否则使用分类类型名称
return (category.Id, GetCategoryTypeName(category.CategoryType));
// 顶级分类按 CategoryType 分组使用负数避免与真实ID冲突
return ((long)-category.CategoryType, GetCategoryTypeName(category.CategoryType));
})
.Select(g => new ReportCategoryGroup
{

View File

@ -31,8 +31,8 @@ public class PlannerService : IPlannerService
/// </summary>
private static readonly Dictionary<int, string> BookingStatusNames = new()
{
{ 1, "待确认" },
{ 2, "已确认" },
{ 1, "待联系" },
{ 2, "已联系" },
{ 3, "已完成" },
{ 4, "已取消" }
};

View File

@ -39,8 +39,19 @@
<el-table-column label="配置值" min-width="250">
<template #default="{ row }">
<!-- 图片类型显示预览 -->
<template v-if="isImageConfig(row.configKey)">
<el-image
v-if="row.configValue"
:src="row.configValue"
:preview-src-list="[row.configValue]"
fit="contain"
style="width: 60px; height: 60px;"
/>
<span v-else class="config-value" style="color: #909399; font-style: italic;">点击编辑上传图片</span>
</template>
<!-- 富文本类型显示预览 -->
<template v-if="isRichTextConfig(row.configKey)">
<template v-else-if="isRichTextConfig(row.configKey)">
<span class="config-value rich-text-preview">
{{ getPlainText(row.configValue) || '点击编辑设置内容' }}
</span>
@ -124,6 +135,29 @@
</el-button>
</template>
</el-dialog>
<!-- 图片配置编辑对话框 -->
<el-dialog
v-model="state.imageDialogVisible"
:title="'编辑 - ' + state.editingItem?.description"
width="500px"
:close-on-click-modal="false"
>
<el-form label-width="80px">
<el-form-item label="配置键">
<el-input :model-value="state.editingItem?.configKey" disabled />
</el-form-item>
<el-form-item label="图片">
<ImageUpload v-model="state.imageValue" placeholder="上传客服二维码图片" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleImageCancel">取消</el-button>
<el-button type="primary" :loading="state.saving" @click="handleImageSave">
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
@ -136,6 +170,7 @@ import { reactive, onMounted } from 'vue'
import { Setting, Edit } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { RichTextEditor } from '@/components'
import ImageUpload from '@/components/ImageUpload/index.vue'
import {
getConfigList,
updateConfig,
@ -154,6 +189,8 @@ interface ConfigPageState {
richTextValue: string
dialogVisible: boolean
richTextDialogVisible: boolean
imageDialogVisible: boolean
imageValue: string
saving: boolean
validationError: string
}
@ -161,6 +198,9 @@ interface ConfigPageState {
//
const RICH_TEXT_CONFIG_KEYS = ['user_agreement', 'privacy_policy', 'about_us_content']
//
const IMAGE_CONFIG_KEYS = ['service_qrcode']
// ============ State ============
const state = reactive<ConfigPageState>({
@ -172,6 +212,8 @@ const state = reactive<ConfigPageState>({
richTextValue: '',
dialogVisible: false,
richTextDialogVisible: false,
imageDialogVisible: false,
imageValue: '',
saving: false,
validationError: ''
})
@ -185,6 +227,13 @@ function isRichTextConfig(configKey: string): boolean {
return RICH_TEXT_CONFIG_KEYS.includes(configKey)
}
/**
* 判断是否为图片配置
*/
function isImageConfig(configKey: string): boolean {
return IMAGE_CONFIG_KEYS.includes(configKey)
}
/**
* HTML 中提取纯文本用于预览
*/
@ -300,6 +349,10 @@ function handleEdit(item: ConfigItem) {
//
state.richTextValue = item.configValue || ''
state.richTextDialogVisible = true
} else if (isImageConfig(item.configKey)) {
//
state.imageValue = item.configValue || ''
state.imageDialogVisible = true
} else {
//
state.editValue = item.configValue
@ -379,6 +432,38 @@ async function handleRichTextSave() {
}
}
function handleImageCancel() {
state.imageDialogVisible = false
state.editingItem = null
state.imageValue = ''
}
async function handleImageSave() {
if (!state.editingItem) return
state.saving = true
try {
const res = await updateConfig({
configKey: state.editingItem.configKey,
configValue: state.imageValue
})
if (res.code === 0) {
ElMessage.success('配置更新成功')
handleImageCancel()
await loadConfigList()
} else {
throw new Error(res.message || '更新配置失败')
}
} catch (error) {
const message = error instanceof Error ? error.message : '更新配置失败'
ElMessage.error(message)
} finally {
state.saving = false
}
}
// ============ Lifecycle ============
onMounted(() => {

View File

@ -191,8 +191,8 @@ const statusForm = reactive({
//
const getStatusType = (status: number): '' | 'success' | 'warning' | 'info' | 'danger' => {
const map: Record<number, '' | 'success' | 'warning' | 'info' | 'danger'> = {
1: 'warning', //
2: '', //
1: 'warning', //
2: '', //
3: 'success', //
4: 'info' //
}

View File

@ -111,4 +111,30 @@ public class SystemController : ControllerBase
return ApiResponse<AboutDto>.Fail("获取关于我们失败");
}
}
/// <summary>
/// 获取联系我们信息
/// </summary>
/// <remarks>
/// GET /api/system/getContactInfo
///
/// 从配置表读取客服二维码图片URL
/// 不需要用户登录认证
/// </remarks>
/// <returns>联系我们信息二维码图片URL</returns>
[HttpGet("getContactInfo")]
[ProducesResponseType(typeof(ApiResponse<ContactDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<ContactDto>> GetContactInfo()
{
try
{
var contact = await _systemService.GetContactInfoAsync();
return ApiResponse<ContactDto>.Success(contact);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get contact info");
return ApiResponse<ContactDto>.Fail("获取联系我们信息失败");
}
}
}

View File

@ -33,4 +33,13 @@ public interface ISystemService
/// </remarks>
/// <returns>关于我们数据</returns>
Task<AboutDto> GetAboutAsync();
/// <summary>
/// 获取联系我们信息
/// </summary>
/// <remarks>
/// 从配置表读取客服二维码图片config_key: service_qrcode
/// </remarks>
/// <returns>联系我们数据</returns>
Task<ContactDto> GetContactInfoAsync();
}

View File

@ -17,6 +17,7 @@ public class SystemService : ISystemService
private const string PrivacyPolicyKey = "privacy_policy";
private const string AboutUsKey = "about_us_content";
private const string AppVersionKey = "app_version";
private const string ServiceQrcodeKey = "service_qrcode";
// 默认版本号
private const string DefaultVersion = "1.0.0";
@ -87,4 +88,19 @@ public class SystemService : ISystemService
Version = version ?? DefaultVersion
};
}
/// <inheritdoc />
public async Task<ContactDto> GetContactInfoAsync()
{
_logger.LogDebug("获取联系我们信息");
var qrcodeUrl = await _configService.GetConfigValueAsync(ServiceQrcodeKey);
_logger.LogDebug("获取联系我们完成二维码URL: {Url}", qrcodeUrl ?? "未配置");
return new ContactDto
{
QrcodeUrl = qrcodeUrl ?? string.Empty
};
}
}

View File

@ -544,7 +544,7 @@ public partial class MiAssessmentDbContext : DbContext
.HasComment("政治成绩");
entity.Property(e => e.Status)
.HasDefaultValue(1)
.HasComment("状态1待确认 2已确认 3已完成 4已取消");
.HasComment("状态1待联系 2已联系 3已完成 4已取消");
entity.Property(e => e.CreateTime)
.HasDefaultValueSql("(getdate())")
.HasComment("创建时间");

View File

@ -127,7 +127,7 @@ public class PlannerBooking
public string? Expectation { get; set; }
/// <summary>
/// 状态1待确认 2已确认 3已完成 4已取消
/// 状态1待联系 2已联系 3已完成 4已取消
/// </summary>
public int Status { get; set; } = 1;

View File

@ -0,0 +1,12 @@
namespace MiAssessment.Model.Models.System;
/// <summary>
/// 联系我们数据传输对象
/// </summary>
public class ContactDto
{
/// <summary>
/// 客服二维码图片URL
/// </summary>
public string QrcodeUrl { get; set; } = string.Empty;
}

View File

@ -28,8 +28,17 @@ export function getAbout() {
return get('/system/getAbout')
}
/**
* 获取联系我们信息客服二维码
* @returns {Promise<Object>}
*/
export function getContactInfo() {
return get('/system/getContactInfo')
}
export default {
getAgreement,
getPrivacy,
getAbout
getAbout,
getContactInfo
}

View File

@ -97,6 +97,25 @@
</view>
</view>
</view>
<!-- 联系我们弹窗 -->
<view v-if="contactPopupVisible" class="popup-mask" @click="hideContactPopup">
<view class="contact-popup" @click.stop>
<view class="contact-popup-header">
<text class="contact-popup-title">联系我们</text>
<text class="contact-popup-close" @click="hideContactPopup"></text>
</view>
<view class="contact-popup-body">
<image
class="contact-qrcode"
:src="contactQrcodeUrl"
mode="aspectFit"
@click="handleSaveQrcode"
/>
<text class="contact-tip">长按识别或点击预览保存二维码</text>
</view>
</view>
</view>
</view>
</template>
@ -109,6 +128,7 @@ import { ref, computed, onMounted } from 'vue'
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app'
import { useUserStore } from '@/store/user.js'
import { useNavbar } from '@/composables/useNavbar.js'
import { getContactInfo } from '@/api/system.js'
const userStore = useUserStore()
const { totalNavbarHeight } = useNavbar()
@ -116,6 +136,10 @@ const { totalNavbarHeight } = useNavbar()
// 退
const logoutPopupVisible = ref(false)
//
const contactPopupVisible = ref(false)
const contactQrcodeUrl = ref('')
//
const isLoggedIn = computed(() => userStore.isLoggedIn)
const isPartner = computed(() => userStore.isPartner)
@ -175,14 +199,38 @@ function goAssessmentHistory() {
}
/**
* 联系我们
* 联系我们 - 弹出客服二维码弹窗
*/
function handleContactUs() {
uni.showModal({
title: '联系我们',
content: '如有问题请联系客服微信xxxxxx',
showCancel: false,
confirmText: '我知道了'
async function handleContactUs() {
try {
const res = await getContactInfo()
if (res && res.code === 0 && res.data && res.data.qrcodeUrl) {
contactQrcodeUrl.value = res.data.qrcodeUrl
contactPopupVisible.value = true
} else {
uni.showToast({ title: '暂未配置客服信息', icon: 'none' })
}
} catch (error) {
console.error('获取联系信息失败:', error)
uni.showToast({ title: '获取联系信息失败', icon: 'none' })
}
}
/**
* 关闭联系我们弹窗
*/
function hideContactPopup() {
contactPopupVisible.value = false
}
/**
* 长按保存二维码图片
*/
function handleSaveQrcode() {
if (!contactQrcodeUrl.value) return
uni.previewImage({
urls: [contactQrcodeUrl.value],
current: contactQrcodeUrl.value
})
}
@ -472,4 +520,50 @@ onMounted(() => {
}
}
}
//
.contact-popup {
width: 560rpx;
background-color: $bg-white;
border-radius: $border-radius-xl;
overflow: hidden;
.contact-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: $spacing-xl $spacing-xl 0;
.contact-popup-title {
font-size: $font-size-lg;
font-weight: $font-weight-bold;
color: $text-color;
}
.contact-popup-close {
font-size: $font-size-xl;
color: $text-placeholder;
padding: $spacing-xs;
}
}
.contact-popup-body {
display: flex;
flex-direction: column;
align-items: center;
padding: $spacing-xl;
.contact-qrcode {
width: 400rpx;
height: 400rpx;
border-radius: $border-radius-md;
}
.contact-tip {
margin-top: $spacing-lg;
font-size: $font-size-sm;
color: $text-placeholder;
}
}
}
</style>

View File

@ -612,6 +612,7 @@ onLoad(async (options) => {
padding: $spacing-lg;
padding-bottom: calc(#{$spacing-lg} + env(safe-area-inset-bottom));
background-color: transparent;
z-index: 100;
}
.submit-btn {