1358 lines
55 KiB
C#
1358 lines
55 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Logic.CrashSight;
|
|
using Logic.Multilingual;
|
|
using TH1_Core.Events;
|
|
using TH1_Logic.Oss;
|
|
using TH1_Logic.Questionnaire;
|
|
using TH1Resource;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
#if STEAM_CHANNEL || STEAMWORKS_NET
|
|
using Steamworks;
|
|
#endif
|
|
|
|
namespace TH1_UI.View.Outside
|
|
{
|
|
public class UIOutsideQuestionnaireView : Base.View
|
|
{
|
|
private const string DataAssetPath = "DataAssets/QuestionnaireDataAssets";
|
|
private const string QuestionPrefabPath = "Prefab/UI/Outside/UIOutsideQuestionnaireQuestion";
|
|
private const string OptionPrefabPath = "Prefab/UI/Outside/UIOutsideQuestionnaireOption";
|
|
|
|
public Button CloseButton;
|
|
public Button SubmitButton;
|
|
public Button RefillButton;
|
|
public TextMeshProUGUI TitleText;
|
|
public TextMeshProUGUI DescriptionText;
|
|
public TextMeshProUGUI StatusText;
|
|
public TextMeshProUGUI SubmitButtonText;
|
|
public TextMeshProUGUI RefillButtonText;
|
|
public TextMeshProUGUI CloseButtonText;
|
|
public Transform QuestionListContent;
|
|
public GameObject QuestionPrefab;
|
|
public GameObject OptionPrefab;
|
|
|
|
public ViDelegateAssisstant.Dele OnBtnCloseClick;
|
|
|
|
private readonly List<UIOutsideQuestionnaireQuestionMono> _questionItems = new List<UIOutsideQuestionnaireQuestionMono>();
|
|
private readonly List<QuestionnaireListItem> _listItems = new List<QuestionnaireListItem>();
|
|
|
|
private QuestionnaireDataAssets _dataAssets;
|
|
private QuestionnaireInfo _questionnaireInfo;
|
|
private QuestionnaireAnswerSheet _currentSheet;
|
|
private bool _isUploading;
|
|
private bool _layoutBuilt;
|
|
private bool _showExpired;
|
|
|
|
private Transform _sidebarContent;
|
|
private Transform _rightBody;
|
|
private Transform _rightFooter;
|
|
private TextMeshProUGUI _expiredToggleText;
|
|
private TextMeshProUGUI _rightHeaderTitle;
|
|
private TextMeshProUGUI _rightHeaderSubtitle;
|
|
private Button _recordsButton;
|
|
private Button _startButton;
|
|
private TextMeshProUGUI _recordsButtonText;
|
|
private TextMeshProUGUI _startButtonText;
|
|
|
|
protected override void OnInit()
|
|
{
|
|
base.OnInit();
|
|
EnsureLayout();
|
|
}
|
|
|
|
public void SetContent(ShowUIOutsideQuestionnaire evt)
|
|
{
|
|
EnsureLayout();
|
|
LoadAssetsIfNeeded();
|
|
|
|
CloseButton.onClick.RemoveAllListeners();
|
|
CloseButton.onClick.AddListener(OnCloseClicked);
|
|
SetUploading(false);
|
|
|
|
var selected = ResolveInitialQuestionnaire(evt);
|
|
if (selected != null && selected.GetEffectiveStatus(DateTime.UtcNow) == QuestionnaireStatus.Expired)
|
|
{
|
|
_showExpired = true;
|
|
}
|
|
|
|
BuildQuestionnaireList();
|
|
|
|
if (selected == null)
|
|
{
|
|
ShowEmptyState("暂无可填写问卷");
|
|
return;
|
|
}
|
|
|
|
SelectQuestionnaire(selected);
|
|
}
|
|
|
|
public void OnCloseView()
|
|
{
|
|
}
|
|
|
|
public static string ResolveText(string raw)
|
|
{
|
|
if (string.IsNullOrEmpty(raw)) return string.Empty;
|
|
if (uint.TryParse(raw, out var id))
|
|
{
|
|
return MultilingualManager.Instance.GetMultilingualText(id);
|
|
}
|
|
|
|
return raw;
|
|
}
|
|
|
|
public static TextMeshProUGUI CreateText(Transform parent, string objectName, string text, float fontSize,
|
|
Color color, TextAlignmentOptions alignment)
|
|
{
|
|
var go = new GameObject(objectName, typeof(RectTransform), typeof(TextMeshProUGUI));
|
|
go.transform.SetParent(parent, false);
|
|
var label = go.GetComponent<TextMeshProUGUI>();
|
|
label.text = text;
|
|
label.fontSize = fontSize;
|
|
label.color = color;
|
|
label.alignment = alignment;
|
|
label.enableWordWrapping = false;
|
|
label.raycastTarget = false;
|
|
return label;
|
|
}
|
|
|
|
private void LoadAssetsIfNeeded()
|
|
{
|
|
if (_dataAssets == null)
|
|
{
|
|
_dataAssets = ResourceLoader.Load<QuestionnaireDataAssets>(DataAssetPath);
|
|
}
|
|
|
|
if (QuestionPrefab == null)
|
|
{
|
|
QuestionPrefab = ResourceLoader.Load<GameObject>(QuestionPrefabPath);
|
|
}
|
|
|
|
if (OptionPrefab == null)
|
|
{
|
|
OptionPrefab = ResourceLoader.Load<GameObject>(OptionPrefabPath);
|
|
}
|
|
}
|
|
|
|
private QuestionnaireInfo ResolveInitialQuestionnaire(ShowUIOutsideQuestionnaire evt)
|
|
{
|
|
if (_dataAssets == null)
|
|
{
|
|
Debug.LogError("[UIOutsideQuestionnaireView] QuestionnaireDataAssets is missing.");
|
|
return null;
|
|
}
|
|
|
|
QuestionnaireInfo selected = null;
|
|
if (!string.IsNullOrEmpty(evt.QuestionnaireId))
|
|
{
|
|
_dataAssets.GetQuestionnaireInfo(evt.QuestionnaireId, out selected);
|
|
}
|
|
|
|
selected ??= _dataAssets.GetDefaultQuestionnaire();
|
|
if (selected != null && selected.IsVisibleAt(DateTime.UtcNow)) return selected;
|
|
|
|
return GetVisibleQuestionnaires().FirstOrDefault();
|
|
}
|
|
|
|
private List<QuestionnaireInfo> GetVisibleQuestionnaires()
|
|
{
|
|
return _dataAssets != null
|
|
? _dataAssets.GetVisibleQuestionnaires(DateTime.UtcNow)
|
|
: new List<QuestionnaireInfo>();
|
|
}
|
|
|
|
private void SelectQuestionnaire(QuestionnaireInfo info)
|
|
{
|
|
if (info == null)
|
|
{
|
|
ShowEmptyState("暂无可填写问卷");
|
|
return;
|
|
}
|
|
|
|
_questionnaireInfo = info;
|
|
_currentSheet = QuestionnaireAnswerStore.Instance.GetLatestAnswerSheet(info.QuestionnaireId);
|
|
if (info.GetEffectiveStatus(DateTime.UtcNow) == QuestionnaireStatus.Expired && !_showExpired)
|
|
{
|
|
_showExpired = true;
|
|
BuildQuestionnaireList();
|
|
}
|
|
|
|
UpdateListSelection();
|
|
ShowDetail();
|
|
}
|
|
|
|
private void BuildQuestionnaireList()
|
|
{
|
|
if (_sidebarContent == null) return;
|
|
|
|
DestroyChildren(_sidebarContent);
|
|
_listItems.Clear();
|
|
|
|
var items = GetVisibleQuestionnaires();
|
|
var featured = items.Where(item => item.GetEffectiveStatus(DateTime.UtcNow) == QuestionnaireStatus.Featured).ToList();
|
|
var longTerm = items.Where(item => item.GetEffectiveStatus(DateTime.UtcNow) == QuestionnaireStatus.LongTerm).ToList();
|
|
var expired = items.Where(item => item.GetEffectiveStatus(DateTime.UtcNow) == QuestionnaireStatus.Expired).ToList();
|
|
|
|
CreateSectionHeader(_sidebarContent, "当期问卷");
|
|
CreateQuestionnaireListItems(featured, "暂无当期问卷");
|
|
CreateDivider(_sidebarContent);
|
|
|
|
CreateSectionHeader(_sidebarContent, "长期问卷");
|
|
CreateQuestionnaireListItems(longTerm, "暂无长期问卷");
|
|
CreateDivider(_sidebarContent);
|
|
|
|
CreateExpiredHeader(expired.Count);
|
|
if (_showExpired)
|
|
{
|
|
CreateQuestionnaireListItems(expired, "暂无往期问卷");
|
|
}
|
|
else if (expired.Count > 0)
|
|
{
|
|
var hint = CreateText(_sidebarContent, "ExpiredHiddenHint", $"{expired.Count} 份往期问卷已收起", 17f,
|
|
new Color(0.62f, 0.66f, 0.64f, 1f), TextAlignmentOptions.Left);
|
|
hint.enableWordWrapping = true;
|
|
}
|
|
|
|
if (items.Count == 0)
|
|
{
|
|
var empty = CreateText(_sidebarContent, "EmptyList", "暂无可显示问卷", 18f,
|
|
new Color(0.74f, 0.76f, 0.72f, 1f), TextAlignmentOptions.Left);
|
|
empty.enableWordWrapping = true;
|
|
}
|
|
}
|
|
|
|
private void CreateQuestionnaireListItems(List<QuestionnaireInfo> infos, string emptyText)
|
|
{
|
|
if (infos == null || infos.Count == 0)
|
|
{
|
|
var empty = CreateText(_sidebarContent, "Empty", emptyText, 17f,
|
|
new Color(0.62f, 0.66f, 0.64f, 1f), TextAlignmentOptions.Left);
|
|
empty.enableWordWrapping = true;
|
|
return;
|
|
}
|
|
|
|
foreach (var info in infos)
|
|
{
|
|
CreateQuestionnaireListItem(info);
|
|
}
|
|
}
|
|
|
|
private void CreateQuestionnaireListItem(QuestionnaireInfo info)
|
|
{
|
|
var row = new GameObject("QuestionnaireItem", typeof(RectTransform), typeof(Image), typeof(Button),
|
|
typeof(VerticalLayoutGroup), typeof(LayoutElement));
|
|
row.transform.SetParent(_sidebarContent, false);
|
|
|
|
var image = row.GetComponent<Image>();
|
|
image.color = new Color(1f, 1f, 1f, 0.055f);
|
|
|
|
var layout = row.GetComponent<VerticalLayoutGroup>();
|
|
layout.padding = new RectOffset(12, 12, 10, 10);
|
|
layout.spacing = 4f;
|
|
layout.childControlWidth = true;
|
|
layout.childControlHeight = true;
|
|
layout.childForceExpandWidth = true;
|
|
layout.childForceExpandHeight = false;
|
|
|
|
var layoutElement = row.GetComponent<LayoutElement>();
|
|
layoutElement.minHeight = 76f;
|
|
layoutElement.preferredHeight = 82f;
|
|
|
|
var button = row.GetComponent<Button>();
|
|
button.targetGraphic = image;
|
|
button.onClick.AddListener(() => SelectQuestionnaire(info));
|
|
|
|
var header = new GameObject("Header", typeof(RectTransform), typeof(HorizontalLayoutGroup));
|
|
header.transform.SetParent(row.transform, false);
|
|
var headerLayout = header.GetComponent<HorizontalLayoutGroup>();
|
|
headerLayout.spacing = 8f;
|
|
headerLayout.childControlWidth = true;
|
|
headerLayout.childControlHeight = true;
|
|
headerLayout.childForceExpandWidth = false;
|
|
headerLayout.childForceExpandHeight = false;
|
|
headerLayout.childAlignment = TextAnchor.MiddleLeft;
|
|
|
|
var title = CreateText(header.transform, "Title", ResolveText(info.Title), 18f,
|
|
new Color(0.96f, 0.95f, 0.87f, 1f), TextAlignmentOptions.Left);
|
|
title.enableWordWrapping = true;
|
|
title.fontStyle = FontStyles.Bold;
|
|
title.gameObject.AddComponent<LayoutElement>().flexibleWidth = 1f;
|
|
|
|
var badge = CreateText(header.transform, "Badge", GetStatusText(info), 15f,
|
|
GetStatusColor(info), TextAlignmentOptions.Right);
|
|
badge.gameObject.AddComponent<LayoutElement>().preferredWidth = 56f;
|
|
|
|
var meta = CreateText(row.transform, "Meta", BuildListMetaText(info), 15f,
|
|
new Color(0.66f, 0.7f, 0.68f, 1f), TextAlignmentOptions.Left);
|
|
meta.enableWordWrapping = true;
|
|
|
|
_listItems.Add(new QuestionnaireListItem
|
|
{
|
|
Info = info,
|
|
Background = image,
|
|
TitleText = title,
|
|
BadgeText = badge,
|
|
MetaText = meta
|
|
});
|
|
}
|
|
|
|
private void ShowDetail()
|
|
{
|
|
if (_questionnaireInfo == null)
|
|
{
|
|
ShowEmptyState("暂无可填写问卷");
|
|
return;
|
|
}
|
|
|
|
ClearRight();
|
|
var sheets = QuestionnaireAnswerStore.Instance.GetAnswerSheets(_questionnaireInfo.QuestionnaireId);
|
|
_currentSheet = sheets.FirstOrDefault();
|
|
|
|
SetRightHeader(ResolveText(_questionnaireInfo.Title), GetStatusText(_questionnaireInfo));
|
|
|
|
TitleText = CreateText(_rightBody, "DetailTitle", ResolveText(_questionnaireInfo.Title), 34f,
|
|
new Color(0.98f, 0.96f, 0.86f, 1f), TextAlignmentOptions.Left);
|
|
TitleText.fontStyle = FontStyles.Bold;
|
|
TitleText.enableWordWrapping = true;
|
|
|
|
DescriptionText = CreateText(_rightBody, "Description", ResolveText(_questionnaireInfo.Description), 20f,
|
|
new Color(0.8f, 0.83f, 0.79f, 1f), TextAlignmentOptions.Left);
|
|
DescriptionText.enableWordWrapping = true;
|
|
|
|
CreateInfoStrip(_rightBody, BuildDetailMetaText(_questionnaireInfo, sheets));
|
|
|
|
var latestMessage = ResolveSubmittedMessage(_currentSheet);
|
|
if (!string.IsNullOrEmpty(latestMessage))
|
|
{
|
|
StatusText = CreateText(_rightBody, "LatestStatus", latestMessage, 18f,
|
|
new Color(0.36f, 0.86f, 0.76f, 1f), TextAlignmentOptions.Left);
|
|
StatusText.enableWordWrapping = true;
|
|
}
|
|
|
|
if (_currentSheet != null)
|
|
{
|
|
CreateLatestRecordPreview(_currentSheet);
|
|
}
|
|
|
|
var canStart = CanStartNewSubmission(_questionnaireInfo, sheets, out var blockReason);
|
|
if (!canStart && !string.IsNullOrEmpty(blockReason))
|
|
{
|
|
CreateInfoStrip(_rightBody, blockReason, new Color(0.9f, 0.68f, 0.36f, 1f));
|
|
}
|
|
|
|
if (sheets.Count > 0 && _questionnaireInfo.CanViewHistory)
|
|
{
|
|
_recordsButton = CreateButton(_rightFooter, "RecordsButton", $"查看记录 ({sheets.Count})", false,
|
|
out _recordsButtonText, 170f);
|
|
_recordsButton.onClick.AddListener(ShowRecords);
|
|
}
|
|
|
|
var startText = sheets.Count > 0 ? ResolveText(_questionnaireInfo.ResubmitButtonText) : ResolveText(_questionnaireInfo.SubmitButtonText);
|
|
if (string.IsNullOrEmpty(startText))
|
|
{
|
|
startText = sheets.Count > 0 ? "再次填写" : "开始填写";
|
|
}
|
|
|
|
_startButton = CreateButton(_rightFooter, "StartButton", startText, true, out _startButtonText, 160f);
|
|
_startButton.interactable = canStart;
|
|
_startButton.onClick.AddListener(ShowForm);
|
|
}
|
|
|
|
private void ShowForm()
|
|
{
|
|
if (_questionnaireInfo == null) return;
|
|
|
|
var sheets = QuestionnaireAnswerStore.Instance.GetAnswerSheets(_questionnaireInfo.QuestionnaireId);
|
|
if (!CanStartNewSubmission(_questionnaireInfo, sheets, out var blockReason))
|
|
{
|
|
ShowDetail();
|
|
SetStatus(blockReason);
|
|
return;
|
|
}
|
|
|
|
ClearRight();
|
|
SetRightHeader("填写问卷", ResolveText(_questionnaireInfo.Title));
|
|
|
|
TitleText = CreateText(_rightBody, "FormTitle", ResolveText(_questionnaireInfo.Title), 30f,
|
|
new Color(0.98f, 0.96f, 0.86f, 1f), TextAlignmentOptions.Left);
|
|
TitleText.fontStyle = FontStyles.Bold;
|
|
TitleText.enableWordWrapping = true;
|
|
|
|
var hint = CreateText(_rightBody, "FormHint", "提交后会保留为一条新的填写记录,可在历史中只读查看。", 18f,
|
|
new Color(0.73f, 0.77f, 0.74f, 1f), TextAlignmentOptions.Left);
|
|
hint.enableWordWrapping = true;
|
|
|
|
CreateQuestionListRoot();
|
|
BuildQuestions(_questionnaireInfo.Questions, false, null);
|
|
|
|
StatusText = CreateText(_rightFooter, "Status", "", 17f,
|
|
new Color(0.36f, 0.86f, 0.76f, 1f), TextAlignmentOptions.Left);
|
|
StatusText.enableWordWrapping = true;
|
|
StatusText.gameObject.AddComponent<LayoutElement>().flexibleWidth = 1f;
|
|
|
|
var backButton = CreateButton(_rightFooter, "BackButton", "返回", false, out _, 120f);
|
|
backButton.onClick.AddListener(ShowDetail);
|
|
|
|
RefillButton = CreateButton(_rightFooter, "RefillButton", "清空", false, out RefillButtonText, 120f);
|
|
RefillButton.onClick.AddListener(OnRefillClicked);
|
|
|
|
var submitText = ResolveText(_questionnaireInfo.SubmitButtonText);
|
|
if (string.IsNullOrEmpty(submitText)) submitText = "提交";
|
|
SubmitButton = CreateButton(_rightFooter, "SubmitButton", submitText, true, out SubmitButtonText, 140f);
|
|
SubmitButton.onClick.AddListener(OnSubmitClicked);
|
|
SetUploading(false);
|
|
SetStatus(string.Empty);
|
|
}
|
|
|
|
private void ShowRecords()
|
|
{
|
|
if (_questionnaireInfo == null) return;
|
|
|
|
ClearRight();
|
|
SetRightHeader("填写记录", ResolveText(_questionnaireInfo.Title));
|
|
|
|
var sheets = QuestionnaireAnswerStore.Instance.GetAnswerSheets(_questionnaireInfo.QuestionnaireId);
|
|
TitleText = CreateText(_rightBody, "RecordsTitle", "填写记录", 31f,
|
|
new Color(0.98f, 0.96f, 0.86f, 1f), TextAlignmentOptions.Left);
|
|
TitleText.fontStyle = FontStyles.Bold;
|
|
|
|
if (sheets.Count == 0)
|
|
{
|
|
var empty = CreateText(_rightBody, "EmptyRecords", "还没有填写记录。", 20f,
|
|
new Color(0.74f, 0.78f, 0.74f, 1f), TextAlignmentOptions.Left);
|
|
empty.enableWordWrapping = true;
|
|
}
|
|
else
|
|
{
|
|
for (var i = 0; i < sheets.Count; i++)
|
|
{
|
|
CreateRecordRow(sheets[i], i + 1);
|
|
}
|
|
}
|
|
|
|
var backButton = CreateButton(_rightFooter, "BackButton", "返回", false, out _, 120f);
|
|
backButton.onClick.AddListener(ShowDetail);
|
|
}
|
|
|
|
private void ShowReadonlyRecord(QuestionnaireAnswerSheet sheet)
|
|
{
|
|
if (sheet == null) return;
|
|
|
|
ClearRight();
|
|
SetRightHeader("查看记录", ResolveText(_questionnaireInfo?.Title));
|
|
|
|
var snapshotTitle = sheet.DefinitionSnapshot != null && !string.IsNullOrEmpty(sheet.DefinitionSnapshot.Title)
|
|
? sheet.DefinitionSnapshot.Title
|
|
: ResolveText(_questionnaireInfo?.Title);
|
|
|
|
TitleText = CreateText(_rightBody, "RecordTitle", snapshotTitle, 30f,
|
|
new Color(0.98f, 0.96f, 0.86f, 1f), TextAlignmentOptions.Left);
|
|
TitleText.fontStyle = FontStyles.Bold;
|
|
TitleText.enableWordWrapping = true;
|
|
|
|
CreateInfoStrip(_rightBody, $"{FormatSubmittedAt(sheet)} · {GetUploadStateText(sheet)} · 只读记录");
|
|
|
|
CreateQuestionListRoot();
|
|
var questions = BuildQuestionsFromSnapshot(sheet.DefinitionSnapshot);
|
|
if (questions == null || questions.Count == 0)
|
|
{
|
|
questions = _questionnaireInfo?.Questions;
|
|
}
|
|
|
|
BuildQuestions(questions, true, sheet);
|
|
|
|
var backButton = CreateButton(_rightFooter, "BackButton", "返回记录", false, out _, 140f);
|
|
backButton.onClick.AddListener(ShowRecords);
|
|
}
|
|
|
|
private void CreateLatestRecordPreview(QuestionnaireAnswerSheet sheet)
|
|
{
|
|
var preview = CreatePanel(_rightBody, "LatestRecordPreview", new Color(0.08f, 0.12f, 0.115f, 0.82f));
|
|
var layout = preview.GetComponent<VerticalLayoutGroup>();
|
|
layout.padding = new RectOffset(16, 16, 14, 14);
|
|
layout.spacing = 6f;
|
|
|
|
var title = CreateText(preview.transform, "Title", "最近一次填写", 20f,
|
|
new Color(0.96f, 0.95f, 0.87f, 1f), TextAlignmentOptions.Left);
|
|
title.fontStyle = FontStyles.Bold;
|
|
|
|
var meta = CreateText(preview.transform, "Meta", $"{FormatSubmittedAt(sheet)} · {GetUploadStateText(sheet)}", 17f,
|
|
new Color(0.7f, 0.75f, 0.71f, 1f), TextAlignmentOptions.Left);
|
|
meta.enableWordWrapping = true;
|
|
}
|
|
|
|
private void CreateRecordRow(QuestionnaireAnswerSheet sheet, int index)
|
|
{
|
|
var row = new GameObject("RecordRow", typeof(RectTransform), typeof(Image), typeof(HorizontalLayoutGroup),
|
|
typeof(LayoutElement));
|
|
row.transform.SetParent(_rightBody, false);
|
|
row.GetComponent<Image>().color = new Color(1f, 1f, 1f, 0.055f);
|
|
|
|
var layout = row.GetComponent<HorizontalLayoutGroup>();
|
|
layout.padding = new RectOffset(16, 14, 12, 12);
|
|
layout.spacing = 14f;
|
|
layout.childControlWidth = true;
|
|
layout.childControlHeight = true;
|
|
layout.childForceExpandWidth = false;
|
|
layout.childForceExpandHeight = false;
|
|
layout.childAlignment = TextAnchor.MiddleLeft;
|
|
|
|
var layoutElement = row.GetComponent<LayoutElement>();
|
|
layoutElement.minHeight = 76f;
|
|
|
|
var textRoot = new GameObject("TextRoot", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(LayoutElement));
|
|
textRoot.transform.SetParent(row.transform, false);
|
|
textRoot.GetComponent<LayoutElement>().flexibleWidth = 1f;
|
|
var textLayout = textRoot.GetComponent<VerticalLayoutGroup>();
|
|
textLayout.spacing = 3f;
|
|
textLayout.childControlWidth = true;
|
|
textLayout.childControlHeight = true;
|
|
textLayout.childForceExpandWidth = true;
|
|
textLayout.childForceExpandHeight = false;
|
|
|
|
var title = CreateText(textRoot.transform, "Title", $"记录 {index}", 20f,
|
|
new Color(0.96f, 0.95f, 0.87f, 1f), TextAlignmentOptions.Left);
|
|
title.fontStyle = FontStyles.Bold;
|
|
|
|
var meta = CreateText(textRoot.transform, "Meta", $"{FormatSubmittedAt(sheet)} · {GetUploadStateText(sheet)}", 17f,
|
|
new Color(0.72f, 0.76f, 0.72f, 1f), TextAlignmentOptions.Left);
|
|
meta.enableWordWrapping = true;
|
|
|
|
var viewButton = CreateButton(row.transform, "ViewButton", "查看", false, out _, 104f);
|
|
viewButton.onClick.AddListener(() => ShowReadonlyRecord(sheet));
|
|
}
|
|
|
|
private void BuildQuestions(IList<QuestionnaireQuestionInfo> questions, bool readOnly, QuestionnaireAnswerSheet sheet)
|
|
{
|
|
if (QuestionListContent == null) return;
|
|
|
|
DestroyChildren(QuestionListContent);
|
|
_questionItems.Clear();
|
|
if (questions == null) return;
|
|
|
|
Dictionary<string, QuestionnaireAnswer> answerDict = null;
|
|
if (sheet?.Answers != null)
|
|
{
|
|
answerDict = sheet.Answers
|
|
.Where(answer => answer != null && !string.IsNullOrEmpty(answer.QuestionId))
|
|
.GroupBy(answer => answer.QuestionId)
|
|
.ToDictionary(group => group.Key, group => group.First());
|
|
}
|
|
|
|
for (var i = 0; i < questions.Count; i++)
|
|
{
|
|
var question = questions[i];
|
|
if (question == null) continue;
|
|
|
|
var item = CreateQuestionItem();
|
|
item.SetContent(i + 1, question, OptionPrefab, readOnly ? null : OnAnswerChanged);
|
|
if (answerDict != null && !string.IsNullOrEmpty(question.QuestionId) &&
|
|
answerDict.TryGetValue(question.QuestionId, out var answer))
|
|
{
|
|
item.ApplyAnswer(answer);
|
|
}
|
|
|
|
item.SetReadOnly(readOnly);
|
|
_questionItems.Add(item);
|
|
}
|
|
}
|
|
|
|
private UIOutsideQuestionnaireQuestionMono CreateQuestionItem()
|
|
{
|
|
GameObject go;
|
|
if (QuestionPrefab != null)
|
|
{
|
|
go = Instantiate(QuestionPrefab, QuestionListContent);
|
|
}
|
|
else
|
|
{
|
|
go = new GameObject("Question", typeof(RectTransform));
|
|
go.transform.SetParent(QuestionListContent, false);
|
|
}
|
|
|
|
var item = go.GetComponent<UIOutsideQuestionnaireQuestionMono>();
|
|
if (item == null)
|
|
{
|
|
item = go.AddComponent<UIOutsideQuestionnaireQuestionMono>();
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
private void OnSubmitClicked()
|
|
{
|
|
if (_isUploading) return;
|
|
_ = SubmitQuestionnaireAsync();
|
|
}
|
|
|
|
private async System.Threading.Tasks.Task SubmitQuestionnaireAsync()
|
|
{
|
|
if (_questionnaireInfo == null) return;
|
|
var sheets = QuestionnaireAnswerStore.Instance.GetAnswerSheets(_questionnaireInfo.QuestionnaireId);
|
|
if (!CanStartNewSubmission(_questionnaireInfo, sheets, out var blockReason))
|
|
{
|
|
SetStatus(blockReason);
|
|
return;
|
|
}
|
|
|
|
if (!ValidateRequiredQuestions())
|
|
{
|
|
SetStatus(ResolveText(_questionnaireInfo.RequiredMessage));
|
|
return;
|
|
}
|
|
|
|
var submitted = false;
|
|
SetUploading(true);
|
|
try
|
|
{
|
|
var sheet = new QuestionnaireAnswerSheet
|
|
{
|
|
QuestionnaireId = _questionnaireInfo.QuestionnaireId,
|
|
QuestionnaireRevision = _questionnaireInfo.Revision,
|
|
ClientVersion = QuestionnaireUploadService.GetCurrentVersion(),
|
|
DefinitionSnapshot = CreateDefinitionSnapshot(_questionnaireInfo),
|
|
Answers = new List<QuestionnaireAnswer>()
|
|
};
|
|
|
|
foreach (var item in _questionItems)
|
|
{
|
|
sheet.Answers.Add(item.CreateAnswer());
|
|
}
|
|
|
|
if (!QuestionnaireAnswerStore.Instance.SaveAnswerSheet(sheet))
|
|
{
|
|
SetStatus(ResolveText(_questionnaireInfo.SaveFailedMessage));
|
|
return;
|
|
}
|
|
|
|
submitted = true;
|
|
_currentSheet = sheet;
|
|
SetStatus(ResolveQuestionnaireMessage(_questionnaireInfo.UploadingMessage,
|
|
_questionnaireInfo.SubmittedMessage));
|
|
|
|
if (!TryGetSteamId(out var steamId))
|
|
{
|
|
QuestionnaireAnswerStore.Instance.SaveUploadResultByResponseId(sheet.ResponseId, false, null,
|
|
"Steam login unavailable");
|
|
SetStatus(ResolveQuestionnaireMessage(_questionnaireInfo.UploadAuthFailedMessage,
|
|
_questionnaireInfo.SubmittedMessage));
|
|
return;
|
|
}
|
|
|
|
await UploadSavedSheetAsync(steamId, sheet);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSystem.LogError($"Questionnaire submit exception: {ex}");
|
|
SetStatus(ResolveQuestionnaireMessage(_questionnaireInfo.UploadFailedMessage,
|
|
_questionnaireInfo.SubmittedMessage));
|
|
}
|
|
finally
|
|
{
|
|
SetUploading(false);
|
|
if (submitted)
|
|
{
|
|
BuildQuestionnaireList();
|
|
SelectQuestionnaire(_questionnaireInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async System.Threading.Tasks.Task UploadSavedSheetAsync(string steamId, QuestionnaireAnswerSheet sheet)
|
|
{
|
|
try
|
|
{
|
|
var version = QuestionnaireUploadService.GetCurrentVersion();
|
|
var bytes = QuestionnaireUploadService.BuildPayloadBytes(steamId, sheet, version);
|
|
if (bytes.Length > QuestionnaireUploadService.MaxQuestionnaireUploadBytes)
|
|
{
|
|
LogSystem.LogError(
|
|
$"Questionnaire upload failed: package size {bytes.Length} exceeds {QuestionnaireUploadService.MaxQuestionnaireUploadBytes}");
|
|
QuestionnaireAnswerStore.Instance.SaveUploadResultByResponseId(sheet.ResponseId, false, null,
|
|
"Questionnaire payload too large");
|
|
SetStatus(ResolveQuestionnaireMessage(_questionnaireInfo.UploadFailedMessage,
|
|
_questionnaireInfo.SubmittedMessage));
|
|
return;
|
|
}
|
|
|
|
var result = await OssManager.Instance.UploadQuestionnaireAnswerAsync(steamId, bytes, version);
|
|
QuestionnaireAnswerStore.Instance.SaveUploadResultByResponseId(sheet.ResponseId, result.success,
|
|
result.objectKey, result.success ? string.Empty : "OSS upload failed");
|
|
|
|
SetStatus(result.success
|
|
? ResolveQuestionnaireMessage(_questionnaireInfo.UploadSuccessMessage, _questionnaireInfo.SubmittedMessage)
|
|
: ResolveQuestionnaireMessage(_questionnaireInfo.UploadFailedMessage, _questionnaireInfo.SubmittedMessage));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSystem.LogError($"Questionnaire submit exception: {ex}");
|
|
QuestionnaireAnswerStore.Instance.SaveUploadResultByResponseId(sheet.ResponseId, false, null, ex.Message);
|
|
SetStatus(ResolveQuestionnaireMessage(_questionnaireInfo.UploadFailedMessage,
|
|
_questionnaireInfo.SubmittedMessage));
|
|
}
|
|
}
|
|
|
|
private bool ValidateRequiredQuestions()
|
|
{
|
|
foreach (var item in _questionItems)
|
|
{
|
|
if (item.QuestionInfo != null && item.QuestionInfo.Required && !item.HasAnswer())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void OnRefillClicked()
|
|
{
|
|
foreach (var item in _questionItems)
|
|
{
|
|
item.ClearAnswer();
|
|
}
|
|
|
|
SetStatus(ResolveText(_questionnaireInfo?.RefillHintText));
|
|
}
|
|
|
|
private void OnAnswerChanged()
|
|
{
|
|
if (!string.IsNullOrEmpty(StatusText?.text))
|
|
{
|
|
SetStatus(string.Empty);
|
|
}
|
|
}
|
|
|
|
private void OnCloseClicked()
|
|
{
|
|
OnBtnCloseClick?.Invoke();
|
|
}
|
|
|
|
private void SetStatus(string text)
|
|
{
|
|
if (StatusText == null) return;
|
|
StatusText.text = text ?? string.Empty;
|
|
StatusText.gameObject.SetActive(!string.IsNullOrEmpty(StatusText.text));
|
|
}
|
|
|
|
private void SetUploading(bool uploading)
|
|
{
|
|
_isUploading = uploading;
|
|
if (SubmitButton != null) SubmitButton.interactable = !uploading;
|
|
if (RefillButton != null) RefillButton.interactable = !uploading;
|
|
if (CloseButton != null) CloseButton.interactable = !uploading;
|
|
if (_recordsButton != null) _recordsButton.interactable = !uploading;
|
|
}
|
|
|
|
private static bool CanStartNewSubmission(QuestionnaireInfo info, List<QuestionnaireAnswerSheet> sheets,
|
|
out string reason)
|
|
{
|
|
reason = string.Empty;
|
|
if (info == null)
|
|
{
|
|
reason = "问卷不存在";
|
|
return false;
|
|
}
|
|
|
|
var now = DateTime.UtcNow;
|
|
if (!info.CanSubmitAt(now))
|
|
{
|
|
reason = info.GetEffectiveStatus(now) == QuestionnaireStatus.Expired
|
|
? "这份问卷已经结束,只能查看历史记录。"
|
|
: "这份问卷当前不可填写。";
|
|
return false;
|
|
}
|
|
|
|
var submittedCount = sheets?.Count ?? 0;
|
|
if (submittedCount > 0 && !info.AllowMultipleSubmissions)
|
|
{
|
|
reason = "这份问卷已经填写过。";
|
|
return false;
|
|
}
|
|
|
|
if (info.MaxSubmissionCount > 0 && submittedCount >= info.MaxSubmissionCount)
|
|
{
|
|
reason = "这份问卷已经达到填写次数上限。";
|
|
return false;
|
|
}
|
|
|
|
if (info.MinSubmitIntervalHours > 0 && sheets != null && sheets.Count > 0)
|
|
{
|
|
var latestUnix = sheets[0].SubmittedAtUnix;
|
|
if (latestUnix > 0)
|
|
{
|
|
var nextAt = DateTimeOffset.FromUnixTimeSeconds(latestUnix).UtcDateTime
|
|
.AddHours(info.MinSubmitIntervalHours);
|
|
if (now < nextAt)
|
|
{
|
|
reason = $"请在 {nextAt.ToLocalTime():yyyy-MM-dd HH:mm} 后再次填写。";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static string ResolveQuestionnaireMessage(string message, string fallback)
|
|
{
|
|
var resolved = ResolveText(message);
|
|
return string.IsNullOrEmpty(resolved) ? ResolveText(fallback) : resolved;
|
|
}
|
|
|
|
private string ResolveSubmittedMessage(QuestionnaireAnswerSheet sheet)
|
|
{
|
|
if (sheet == null || _questionnaireInfo == null) return string.Empty;
|
|
if (!string.IsNullOrEmpty(sheet.LastUploadObjectKey))
|
|
return ResolveQuestionnaireMessage(_questionnaireInfo.UploadSuccessMessage,
|
|
_questionnaireInfo.SubmittedMessage);
|
|
if (!string.IsNullOrEmpty(sheet.LastUploadError))
|
|
return ResolveQuestionnaireMessage(_questionnaireInfo.UploadFailedMessage,
|
|
_questionnaireInfo.SubmittedMessage);
|
|
return ResolveText(_questionnaireInfo.SubmittedMessage);
|
|
}
|
|
|
|
private static bool TryGetSteamId(out string steamId)
|
|
{
|
|
steamId = string.Empty;
|
|
#if STEAM_CHANNEL || STEAMWORKS_NET
|
|
try
|
|
{
|
|
if (!SteamUser.BLoggedOn())
|
|
return false;
|
|
|
|
var id = SteamUser.GetSteamID().m_SteamID;
|
|
if (id == 0)
|
|
return false;
|
|
|
|
steamId = id.ToString();
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSystem.LogWarning($"Questionnaire SteamID unavailable: {ex.Message}");
|
|
return false;
|
|
}
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
private static QuestionnaireDefinitionSnapshot CreateDefinitionSnapshot(QuestionnaireInfo info)
|
|
{
|
|
if (info == null) return null;
|
|
|
|
var snapshot = new QuestionnaireDefinitionSnapshot
|
|
{
|
|
QuestionnaireId = info.QuestionnaireId,
|
|
Revision = info.Revision,
|
|
Title = ResolveText(info.Title),
|
|
Description = ResolveText(info.Description),
|
|
Questions = new List<QuestionnaireQuestionSnapshot>()
|
|
};
|
|
|
|
if (info.Questions == null) return snapshot;
|
|
|
|
foreach (var question in info.Questions)
|
|
{
|
|
if (question == null) continue;
|
|
|
|
var questionSnapshot = new QuestionnaireQuestionSnapshot
|
|
{
|
|
QuestionId = question.QuestionId,
|
|
QuestionType = question.QuestionType,
|
|
Title = ResolveText(question.Title),
|
|
Hint = ResolveText(question.Hint),
|
|
Required = question.Required,
|
|
Options = new List<QuestionnaireOptionSnapshot>()
|
|
};
|
|
|
|
if (question.Options != null)
|
|
{
|
|
foreach (var option in question.Options)
|
|
{
|
|
if (option == null) continue;
|
|
questionSnapshot.Options.Add(new QuestionnaireOptionSnapshot
|
|
{
|
|
OptionId = option.OptionId,
|
|
Text = ResolveText(option.Text)
|
|
});
|
|
}
|
|
}
|
|
|
|
snapshot.Questions.Add(questionSnapshot);
|
|
}
|
|
|
|
return snapshot;
|
|
}
|
|
|
|
private static List<QuestionnaireQuestionInfo> BuildQuestionsFromSnapshot(QuestionnaireDefinitionSnapshot snapshot)
|
|
{
|
|
if (snapshot?.Questions == null || snapshot.Questions.Count == 0) return null;
|
|
|
|
var result = new List<QuestionnaireQuestionInfo>();
|
|
foreach (var question in snapshot.Questions)
|
|
{
|
|
if (question == null) continue;
|
|
|
|
var info = new QuestionnaireQuestionInfo
|
|
{
|
|
QuestionId = question.QuestionId,
|
|
QuestionType = question.QuestionType,
|
|
Title = question.Title,
|
|
Hint = question.Hint,
|
|
Required = question.Required,
|
|
Options = new List<QuestionnaireOptionInfo>()
|
|
};
|
|
|
|
if (question.Options != null)
|
|
{
|
|
foreach (var option in question.Options)
|
|
{
|
|
if (option == null) continue;
|
|
info.Options.Add(new QuestionnaireOptionInfo
|
|
{
|
|
OptionId = option.OptionId,
|
|
Text = option.Text
|
|
});
|
|
}
|
|
}
|
|
|
|
result.Add(info);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private void EnsureLayout()
|
|
{
|
|
var rootRect = GetComponent<RectTransform>();
|
|
rootRect.anchorMin = Vector2.zero;
|
|
rootRect.anchorMax = Vector2.one;
|
|
rootRect.offsetMin = Vector2.zero;
|
|
rootRect.offsetMax = Vector2.zero;
|
|
|
|
var canvasGroup = GetComponent<CanvasGroup>();
|
|
if (canvasGroup == null)
|
|
{
|
|
gameObject.AddComponent<CanvasGroup>();
|
|
}
|
|
|
|
var background = GetComponent<Image>();
|
|
if (background == null)
|
|
{
|
|
background = gameObject.AddComponent<Image>();
|
|
}
|
|
|
|
background.color = new Color(0.035f, 0.045f, 0.047f, 0.96f);
|
|
|
|
if (_layoutBuilt) return;
|
|
_layoutBuilt = true;
|
|
|
|
CreateFloatingCloseButton();
|
|
CreateMainLayout();
|
|
}
|
|
|
|
private void CreateFloatingCloseButton()
|
|
{
|
|
CloseButton = CreateButton(transform, "CloseButton", "X", false, out CloseButtonText, 44f);
|
|
var rect = CloseButton.GetComponent<RectTransform>();
|
|
rect.anchorMin = new Vector2(1f, 1f);
|
|
rect.anchorMax = new Vector2(1f, 1f);
|
|
rect.pivot = new Vector2(1f, 1f);
|
|
rect.anchoredPosition = new Vector2(-32f, -28f);
|
|
rect.sizeDelta = new Vector2(44f, 44f);
|
|
var layout = CloseButton.GetComponent<LayoutElement>();
|
|
if (layout != null)
|
|
{
|
|
Destroy(layout);
|
|
}
|
|
}
|
|
|
|
private void CreateMainLayout()
|
|
{
|
|
var root = new GameObject("QuestionnaireCenter", typeof(RectTransform), typeof(HorizontalLayoutGroup));
|
|
root.transform.SetParent(transform, false);
|
|
var rect = root.GetComponent<RectTransform>();
|
|
rect.anchorMin = Vector2.zero;
|
|
rect.anchorMax = Vector2.one;
|
|
rect.offsetMin = new Vector2(46f, 38f);
|
|
rect.offsetMax = new Vector2(-54f, -38f);
|
|
|
|
var layout = root.GetComponent<HorizontalLayoutGroup>();
|
|
layout.spacing = 16f;
|
|
layout.childControlWidth = true;
|
|
layout.childControlHeight = true;
|
|
layout.childForceExpandWidth = false;
|
|
layout.childForceExpandHeight = true;
|
|
|
|
CreateSidebar(root.transform);
|
|
CreateRightPanel(root.transform);
|
|
}
|
|
|
|
private void CreateSidebar(Transform parent)
|
|
{
|
|
var sidebar = CreatePanel(parent, "Sidebar", new Color(0.07f, 0.095f, 0.09f, 0.95f), 326f);
|
|
var layout = sidebar.GetComponent<VerticalLayoutGroup>();
|
|
layout.padding = new RectOffset(18, 18, 18, 18);
|
|
layout.spacing = 12f;
|
|
|
|
var title = CreateText(sidebar.transform, "Title", "问卷中心", 30f,
|
|
new Color(0.98f, 0.96f, 0.86f, 1f), TextAlignmentOptions.Left);
|
|
title.fontStyle = FontStyles.Bold;
|
|
|
|
var subtitle = CreateText(sidebar.transform, "Subtitle", "当期反馈 / 长期收集 / 往期记录", 17f,
|
|
new Color(0.68f, 0.73f, 0.69f, 1f), TextAlignmentOptions.Left);
|
|
subtitle.enableWordWrapping = true;
|
|
|
|
CreateScroll(sidebar.transform, "SidebarScroll", out _sidebarContent);
|
|
}
|
|
|
|
private void CreateRightPanel(Transform parent)
|
|
{
|
|
var right = CreatePanel(parent, "RightPanel", new Color(0.055f, 0.066f, 0.064f, 0.95f));
|
|
var rightLayoutElement = right.GetComponent<LayoutElement>();
|
|
rightLayoutElement.flexibleWidth = 1f;
|
|
|
|
var layout = right.GetComponent<VerticalLayoutGroup>();
|
|
layout.padding = new RectOffset(24, 24, 22, 20);
|
|
layout.spacing = 14f;
|
|
|
|
var header = new GameObject("Header", typeof(RectTransform), typeof(VerticalLayoutGroup));
|
|
header.transform.SetParent(right.transform, false);
|
|
var headerLayout = header.GetComponent<VerticalLayoutGroup>();
|
|
headerLayout.spacing = 2f;
|
|
headerLayout.childControlWidth = true;
|
|
headerLayout.childControlHeight = true;
|
|
headerLayout.childForceExpandWidth = true;
|
|
headerLayout.childForceExpandHeight = false;
|
|
|
|
_rightHeaderTitle = CreateText(header.transform, "Title", "", 22f,
|
|
new Color(0.36f, 0.86f, 0.76f, 1f), TextAlignmentOptions.Left);
|
|
_rightHeaderTitle.fontStyle = FontStyles.Bold;
|
|
_rightHeaderSubtitle = CreateText(header.transform, "Subtitle", "", 16f,
|
|
new Color(0.64f, 0.69f, 0.66f, 1f), TextAlignmentOptions.Left);
|
|
_rightHeaderSubtitle.enableWordWrapping = true;
|
|
|
|
CreateScroll(right.transform, "BodyScroll", out _rightBody);
|
|
|
|
var footer = new GameObject("Footer", typeof(RectTransform), typeof(HorizontalLayoutGroup), typeof(LayoutElement));
|
|
footer.transform.SetParent(right.transform, false);
|
|
_rightFooter = footer.transform;
|
|
var footerLayout = footer.GetComponent<HorizontalLayoutGroup>();
|
|
footerLayout.spacing = 12f;
|
|
footerLayout.childControlWidth = true;
|
|
footerLayout.childControlHeight = true;
|
|
footerLayout.childForceExpandWidth = false;
|
|
footerLayout.childForceExpandHeight = false;
|
|
footerLayout.childAlignment = TextAnchor.MiddleRight;
|
|
var footerLayoutElement = footer.GetComponent<LayoutElement>();
|
|
footerLayoutElement.minHeight = 48f;
|
|
footerLayoutElement.preferredHeight = 50f;
|
|
}
|
|
|
|
private void CreateQuestionListRoot()
|
|
{
|
|
var root = new GameObject("QuestionListContent", typeof(RectTransform), typeof(VerticalLayoutGroup),
|
|
typeof(ContentSizeFitter));
|
|
root.transform.SetParent(_rightBody, false);
|
|
var layout = root.GetComponent<VerticalLayoutGroup>();
|
|
layout.spacing = 10f;
|
|
layout.childControlWidth = true;
|
|
layout.childControlHeight = true;
|
|
layout.childForceExpandWidth = true;
|
|
layout.childForceExpandHeight = false;
|
|
root.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
QuestionListContent = root.transform;
|
|
}
|
|
|
|
private void ClearRight()
|
|
{
|
|
DestroyChildren(_rightBody);
|
|
DestroyChildren(_rightFooter);
|
|
_questionItems.Clear();
|
|
QuestionListContent = null;
|
|
SubmitButton = null;
|
|
RefillButton = null;
|
|
StatusText = null;
|
|
_recordsButton = null;
|
|
_startButton = null;
|
|
}
|
|
|
|
private void ShowEmptyState(string text)
|
|
{
|
|
ClearRight();
|
|
SetRightHeader("问卷中心", string.Empty);
|
|
var empty = CreateText(_rightBody, "EmptyState", text, 24f,
|
|
new Color(0.8f, 0.82f, 0.78f, 1f), TextAlignmentOptions.Center);
|
|
empty.enableWordWrapping = true;
|
|
}
|
|
|
|
private void SetRightHeader(string title, string subtitle)
|
|
{
|
|
if (_rightHeaderTitle != null) _rightHeaderTitle.text = title ?? string.Empty;
|
|
if (_rightHeaderSubtitle != null) _rightHeaderSubtitle.text = subtitle ?? string.Empty;
|
|
}
|
|
|
|
private GameObject CreatePanel(Transform parent, string objectName, Color color, float preferredWidth = 0f)
|
|
{
|
|
var go = new GameObject(objectName, typeof(RectTransform), typeof(Image), typeof(VerticalLayoutGroup),
|
|
typeof(LayoutElement));
|
|
go.transform.SetParent(parent, false);
|
|
var image = go.GetComponent<Image>();
|
|
image.color = color;
|
|
|
|
var layout = go.GetComponent<VerticalLayoutGroup>();
|
|
layout.childControlWidth = true;
|
|
layout.childControlHeight = true;
|
|
layout.childForceExpandWidth = true;
|
|
layout.childForceExpandHeight = false;
|
|
|
|
var layoutElement = go.GetComponent<LayoutElement>();
|
|
if (preferredWidth > 0f)
|
|
{
|
|
layoutElement.minWidth = preferredWidth;
|
|
layoutElement.preferredWidth = preferredWidth;
|
|
}
|
|
|
|
return go;
|
|
}
|
|
|
|
private static void CreateScroll(Transform parent, string objectName, out Transform content)
|
|
{
|
|
var scrollRoot = new GameObject(objectName, typeof(RectTransform), typeof(ScrollRect), typeof(LayoutElement));
|
|
scrollRoot.transform.SetParent(parent, false);
|
|
var layoutElement = scrollRoot.GetComponent<LayoutElement>();
|
|
layoutElement.flexibleHeight = 1f;
|
|
layoutElement.minHeight = 120f;
|
|
|
|
var viewport = new GameObject("Viewport", typeof(RectTransform), typeof(Image), typeof(Mask));
|
|
viewport.transform.SetParent(scrollRoot.transform, false);
|
|
var viewportRect = viewport.GetComponent<RectTransform>();
|
|
viewportRect.anchorMin = Vector2.zero;
|
|
viewportRect.anchorMax = Vector2.one;
|
|
viewportRect.offsetMin = Vector2.zero;
|
|
viewportRect.offsetMax = Vector2.zero;
|
|
viewport.GetComponent<Image>().color = new Color(1f, 1f, 1f, 0.01f);
|
|
viewport.GetComponent<Mask>().showMaskGraphic = false;
|
|
|
|
var contentRoot = new GameObject("Content", typeof(RectTransform), typeof(VerticalLayoutGroup),
|
|
typeof(ContentSizeFitter));
|
|
contentRoot.transform.SetParent(viewport.transform, false);
|
|
var contentRect = contentRoot.GetComponent<RectTransform>();
|
|
contentRect.anchorMin = new Vector2(0f, 1f);
|
|
contentRect.anchorMax = new Vector2(1f, 1f);
|
|
contentRect.pivot = new Vector2(0.5f, 1f);
|
|
contentRect.offsetMin = Vector2.zero;
|
|
contentRect.offsetMax = Vector2.zero;
|
|
|
|
var layout = contentRoot.GetComponent<VerticalLayoutGroup>();
|
|
layout.spacing = 10f;
|
|
layout.childControlWidth = true;
|
|
layout.childControlHeight = true;
|
|
layout.childForceExpandWidth = true;
|
|
layout.childForceExpandHeight = false;
|
|
contentRoot.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
|
|
var scrollRect = scrollRoot.GetComponent<ScrollRect>();
|
|
scrollRect.viewport = viewportRect;
|
|
scrollRect.content = contentRect;
|
|
scrollRect.horizontal = false;
|
|
scrollRect.vertical = true;
|
|
scrollRect.scrollSensitivity = 30f;
|
|
scrollRect.movementType = ScrollRect.MovementType.Clamped;
|
|
|
|
content = contentRoot.transform;
|
|
}
|
|
|
|
private void CreateSectionHeader(Transform parent, string text)
|
|
{
|
|
var header = CreateText(parent, "SectionHeader", text, 18f,
|
|
new Color(0.36f, 0.86f, 0.76f, 1f), TextAlignmentOptions.Left);
|
|
header.fontStyle = FontStyles.Bold;
|
|
}
|
|
|
|
private void CreateExpiredHeader(int expiredCount)
|
|
{
|
|
var button = CreateButton(_sidebarContent, "ExpiredToggle", _showExpired ? "往期问卷 v" : "往期问卷 >",
|
|
false, out _expiredToggleText, 0f);
|
|
var layout = button.GetComponent<LayoutElement>();
|
|
layout.minHeight = 34f;
|
|
layout.preferredHeight = 36f;
|
|
if (expiredCount > 0)
|
|
{
|
|
_expiredToggleText.text = _showExpired ? $"往期问卷 ({expiredCount}) v" : $"往期问卷 ({expiredCount}) >";
|
|
}
|
|
|
|
button.onClick.AddListener(() =>
|
|
{
|
|
_showExpired = !_showExpired;
|
|
BuildQuestionnaireList();
|
|
UpdateListSelection();
|
|
});
|
|
}
|
|
|
|
private void CreateDivider(Transform parent)
|
|
{
|
|
var divider = new GameObject("Divider", typeof(RectTransform), typeof(Image), typeof(LayoutElement));
|
|
divider.transform.SetParent(parent, false);
|
|
divider.GetComponent<Image>().color = new Color(1f, 1f, 1f, 0.08f);
|
|
var layout = divider.GetComponent<LayoutElement>();
|
|
layout.minHeight = 1f;
|
|
layout.preferredHeight = 1f;
|
|
}
|
|
|
|
private void CreateInfoStrip(Transform parent, string text)
|
|
{
|
|
CreateInfoStrip(parent, text, new Color(0.7f, 0.75f, 0.72f, 1f));
|
|
}
|
|
|
|
private void CreateInfoStrip(Transform parent, string text, Color textColor)
|
|
{
|
|
var strip = new GameObject("InfoStrip", typeof(RectTransform), typeof(Image), typeof(LayoutElement));
|
|
strip.transform.SetParent(parent, false);
|
|
strip.GetComponent<Image>().color = new Color(1f, 1f, 1f, 0.055f);
|
|
var layoutElement = strip.GetComponent<LayoutElement>();
|
|
layoutElement.minHeight = 46f;
|
|
|
|
var label = CreateText(strip.transform, "Label", text, 18f, textColor, TextAlignmentOptions.Left);
|
|
label.enableWordWrapping = true;
|
|
label.rectTransform.anchorMin = Vector2.zero;
|
|
label.rectTransform.anchorMax = Vector2.one;
|
|
label.rectTransform.offsetMin = new Vector2(14f, 8f);
|
|
label.rectTransform.offsetMax = new Vector2(-14f, -8f);
|
|
}
|
|
|
|
private static Button CreateButton(Transform parent, string objectName, string text, bool primary,
|
|
out TextMeshProUGUI label, float preferredWidth)
|
|
{
|
|
var go = new GameObject(objectName, typeof(RectTransform), typeof(Image), typeof(Button), typeof(LayoutElement));
|
|
go.transform.SetParent(parent, false);
|
|
|
|
var layout = go.GetComponent<LayoutElement>();
|
|
layout.minWidth = preferredWidth > 0f ? preferredWidth : 120f;
|
|
layout.preferredWidth = preferredWidth > 0f ? preferredWidth : 150f;
|
|
layout.minHeight = 42f;
|
|
layout.preferredHeight = 46f;
|
|
|
|
var image = go.GetComponent<Image>();
|
|
image.color = primary ? new Color(0.15f, 0.56f, 0.49f, 1f) : new Color(1f, 1f, 1f, 0.10f);
|
|
|
|
var button = go.GetComponent<Button>();
|
|
button.transition = Selectable.Transition.ColorTint;
|
|
button.targetGraphic = image;
|
|
|
|
label = CreateText(go.transform, "Label", text, 19f,
|
|
new Color(0.98f, 0.98f, 0.92f, 1f), TextAlignmentOptions.Center);
|
|
label.rectTransform.anchorMin = Vector2.zero;
|
|
label.rectTransform.anchorMax = Vector2.one;
|
|
label.rectTransform.offsetMin = Vector2.zero;
|
|
label.rectTransform.offsetMax = Vector2.zero;
|
|
label.enableWordWrapping = false;
|
|
return button;
|
|
}
|
|
|
|
private void UpdateListSelection()
|
|
{
|
|
foreach (var item in _listItems)
|
|
{
|
|
var selected = item.Info == _questionnaireInfo;
|
|
item.Background.color = selected
|
|
? new Color(0.16f, 0.38f, 0.34f, 0.94f)
|
|
: new Color(1f, 1f, 1f, 0.055f);
|
|
item.TitleText.color = selected
|
|
? new Color(1f, 0.98f, 0.88f, 1f)
|
|
: new Color(0.92f, 0.92f, 0.86f, 1f);
|
|
}
|
|
}
|
|
|
|
private static string BuildListMetaText(QuestionnaireInfo info)
|
|
{
|
|
var sheets = QuestionnaireAnswerStore.Instance.GetAnswerSheets(info.QuestionnaireId);
|
|
var questionCount = info.Questions?.Count ?? 0;
|
|
if (sheets.Count == 0) return $"{questionCount} 题 · 未填写";
|
|
return $"{questionCount} 题 · 已填写 {sheets.Count} 次";
|
|
}
|
|
|
|
private static string BuildDetailMetaText(QuestionnaireInfo info, List<QuestionnaireAnswerSheet> sheets)
|
|
{
|
|
var questionCount = info.Questions?.Count ?? 0;
|
|
var count = sheets?.Count ?? 0;
|
|
var status = GetStatusText(info);
|
|
if (count == 0) return $"{status} · {questionCount} 题 · 未填写";
|
|
return $"{status} · {questionCount} 题 · 已填写 {count} 次 · 最近 {FormatSubmittedAt(sheets[0])}";
|
|
}
|
|
|
|
private static string GetStatusText(QuestionnaireInfo info)
|
|
{
|
|
if (info == null) return string.Empty;
|
|
return info.GetEffectiveStatus(DateTime.UtcNow) switch
|
|
{
|
|
QuestionnaireStatus.Featured => "当期",
|
|
QuestionnaireStatus.LongTerm => "长期",
|
|
QuestionnaireStatus.Expired => "往期",
|
|
_ => "隐藏"
|
|
};
|
|
}
|
|
|
|
private static Color GetStatusColor(QuestionnaireInfo info)
|
|
{
|
|
if (info == null) return new Color(0.62f, 0.66f, 0.64f, 1f);
|
|
return info.GetEffectiveStatus(DateTime.UtcNow) switch
|
|
{
|
|
QuestionnaireStatus.Featured => new Color(0.42f, 0.9f, 0.78f, 1f),
|
|
QuestionnaireStatus.LongTerm => new Color(0.8f, 0.82f, 0.7f, 1f),
|
|
QuestionnaireStatus.Expired => new Color(0.68f, 0.68f, 0.66f, 1f),
|
|
_ => new Color(0.62f, 0.66f, 0.64f, 1f)
|
|
};
|
|
}
|
|
|
|
private static string FormatSubmittedAt(QuestionnaireAnswerSheet sheet)
|
|
{
|
|
if (sheet == null) return string.Empty;
|
|
if (sheet.SubmittedAtUnix > 0)
|
|
{
|
|
return DateTimeOffset.FromUnixTimeSeconds(sheet.SubmittedAtUnix).ToLocalTime()
|
|
.ToString("yyyy-MM-dd HH:mm");
|
|
}
|
|
|
|
if (DateTime.TryParse(sheet.SubmittedAtUtc, out var parsed))
|
|
{
|
|
return parsed.ToLocalTime().ToString("yyyy-MM-dd HH:mm");
|
|
}
|
|
|
|
return "未知时间";
|
|
}
|
|
|
|
private static string GetUploadStateText(QuestionnaireAnswerSheet sheet)
|
|
{
|
|
if (sheet == null) return string.Empty;
|
|
if (!string.IsNullOrEmpty(sheet.LastUploadObjectKey)) return "已上传";
|
|
if (!string.IsNullOrEmpty(sheet.LastUploadError)) return "仅本地保存";
|
|
return "等待上传结果";
|
|
}
|
|
|
|
private static void DestroyChildren(Transform parent)
|
|
{
|
|
if (parent == null) return;
|
|
for (var i = parent.childCount - 1; i >= 0; i--)
|
|
{
|
|
Destroy(parent.GetChild(i).gameObject);
|
|
}
|
|
}
|
|
|
|
private class QuestionnaireListItem
|
|
{
|
|
public QuestionnaireInfo Info;
|
|
public Image Background;
|
|
public TextMeshProUGUI TitleText;
|
|
public TextMeshProUGUI BadgeText;
|
|
public TextMeshProUGUI MetaText;
|
|
}
|
|
}
|
|
}
|