UseGuidance.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. <script setup lang="ts" generic="T extends Record<string, any>">
  2. import { computed, ref, useTemplateRef } from 'vue';
  3. import { useRoute, useRouter } from 'vue-router';
  4. import { message } from 'ant-design-vue';
  5. import ConfirmModal from '@/components/ConfirmModal.vue';
  6. import SvgIcon from '@/components/SvgIcon.vue';
  7. import { t } from '@/i18n';
  8. import { addUnit } from '@/utils';
  9. import type { CSSProperties } from 'vue';
  10. import type { FormInstance } from 'ant-design-vue';
  11. import type { FormRules, UseGuideStepItem, UseGuideStepItemInstance } from '@/types';
  12. interface Props {
  13. title: string;
  14. steps: UseGuideStepItem[];
  15. form: T;
  16. rules: FormRules<T>;
  17. contentOffset?: number;
  18. headerMargin?: number;
  19. }
  20. const props = defineProps<Props>();
  21. const route = useRoute();
  22. const router = useRouter();
  23. const current = ref(0);
  24. const currentStep = computed(() => {
  25. return props.steps[current.value];
  26. });
  27. const isFirstStep = computed(() => {
  28. return current.value === 0;
  29. });
  30. const contentStyle = computed<CSSProperties>(() => {
  31. // const contentOffset = props.steps[current.value].contentOffset ?? props.contentOffset;
  32. return {
  33. // paddingLeft: addUnit(contentOffset ?? 312),
  34. };
  35. });
  36. const headerStyle = computed<CSSProperties>(() => {
  37. const dividerWidth = currentStep.value.hideHeaderDivider ? 0 : 1;
  38. const headerMargin = currentStep.value.headerMargin ?? props.headerMargin;
  39. return {
  40. borderBottomWidth: addUnit(dividerWidth),
  41. marginBottom: addUnit(headerMargin ?? 40),
  42. };
  43. });
  44. const goToStep = (index: number) => {
  45. current.value = index;
  46. };
  47. const goNextStep = () => {
  48. current.value++;
  49. };
  50. const goPrevStep = async () => {
  51. if (isFirstStep.value) {
  52. goOutOfUseGuide();
  53. return;
  54. }
  55. const hasGoBack = await stepRef.value?.goBack?.();
  56. if (!hasGoBack) {
  57. current.value--;
  58. }
  59. };
  60. const goOutOfUseGuide = () => {
  61. // 配置完成某一项时,路由查询参数 doneItems 会携带该项的页面路径,据此判断是否已经配置该项
  62. let doneItems = route.query.doneItems ?? '';
  63. if (currentStep.value.isLastStep) {
  64. doneItems += route.path;
  65. }
  66. const donePaths = [...new Set((doneItems as string).split('/'))];
  67. router.replace({
  68. path: '/first-usage',
  69. query: {
  70. doneItems: donePaths.join('/'),
  71. },
  72. });
  73. };
  74. const confirmModalRef = useTemplateRef('confirmModal');
  75. const skipCurrentStep = () => {
  76. confirmModalRef.value?.showView();
  77. };
  78. const formRef = ref<FormInstance>();
  79. const stepRef = useTemplateRef<UseGuideStepItemInstance>('stepItem');
  80. const exportStepContent = async () => {
  81. await stepRef.value?.exportData?.();
  82. };
  83. const finishCurrentStep = async () => {
  84. try {
  85. await formRef.value?.validate();
  86. await stepRef.value?.finish?.();
  87. if (currentStep.value.isLastStep) {
  88. goOutOfUseGuide();
  89. } else {
  90. goNextStep();
  91. }
  92. } catch (err) {
  93. if (err instanceof Error) {
  94. message.error(err.message);
  95. }
  96. console.error(err);
  97. }
  98. };
  99. </script>
  100. <template>
  101. <ALayout class="use-guide-container">
  102. <ALayoutSider class="use-guide-sider" :width="268">
  103. <div class="use-guide-title">{{ title }}</div>
  104. <div class="use-guide-steps">
  105. <ASteps :current="current" :items="steps" direction="vertical" />
  106. </div>
  107. <AButton class="icon-button doc-button">
  108. <SvgIcon name="eye-o" />
  109. {{ $t('common.viewDoc') }}
  110. </AButton>
  111. </ALayoutSider>
  112. <ALayout>
  113. <ALayoutContent class="use-guide-main" :style="contentStyle">
  114. <div v-show="!currentStep.hideHeader" class="step-header" :style="headerStyle">
  115. <div class="step-title">{{ currentStep.stepTitle || currentStep.title }}</div>
  116. <div class="step-description">{{ currentStep.stepDescription }}</div>
  117. </div>
  118. <AForm
  119. ref="formRef"
  120. :model="form"
  121. :rules="rules"
  122. :layout="currentStep.formLayout"
  123. :label-align="currentStep.labelAlign"
  124. :label-col="currentStep.labelCol"
  125. :wrapper-col="currentStep.wrapperCol"
  126. :colon="false"
  127. >
  128. <component
  129. ref="stepItem"
  130. :is="currentStep.component"
  131. :form="form"
  132. :steps="steps"
  133. :step-index="current"
  134. :go-to-step="goToStep"
  135. />
  136. </AForm>
  137. </ALayoutContent>
  138. <ALayoutFooter class="use-guide-footer">
  139. <div class="use-guide-step-button-container">
  140. <div>
  141. <AButton type="text" :disabled="currentStep.isLastStep" @click="skipCurrentStep">
  142. {{ $t('common.skip') }}
  143. </AButton>
  144. <AButton type="text" @click="goPrevStep">{{ $t('common.return') }}</AButton>
  145. </div>
  146. <div>
  147. <AButton v-if="currentStep.exportButtonShow" class="icon-button export-button" @click="exportStepContent">
  148. <SvgIcon name="download" />
  149. {{ $t('common.exportToLocal') }}
  150. </AButton>
  151. <AButton
  152. v-show="!currentStep.nextStepButtonHide"
  153. class="next-step-button"
  154. type="primary"
  155. :disabled="currentStep.nextStepButtonDisabled"
  156. @click="finishCurrentStep"
  157. >
  158. {{ currentStep.nextStepButtonText || t('common.nextStep') }}
  159. </AButton>
  160. </div>
  161. <ConfirmModal
  162. ref="confirmModal"
  163. :title="$t('common.skipConfirm')"
  164. :description-text="$t('common.skipStepConfirm', { name: currentStep.title }) + $t('common.returnFirstUse')"
  165. :icon="{ name: 'exclamation' }"
  166. icon-bg-color="#F56C6C"
  167. @confirm="goOutOfUseGuide"
  168. />
  169. </div>
  170. </ALayoutFooter>
  171. </ALayout>
  172. </ALayout>
  173. </template>
  174. <style lang="scss" scoped>
  175. .use-guide-container {
  176. height: 100%;
  177. }
  178. .use-guide-sider {
  179. padding-top: 48px;
  180. padding-bottom: 40px;
  181. background: #f0f0f0;
  182. :deep(.ant-layout-sider-children) {
  183. display: flex;
  184. flex-direction: column;
  185. align-items: center;
  186. }
  187. .use-guide-title {
  188. margin-bottom: 32px;
  189. text-align: center;
  190. }
  191. .use-guide-steps {
  192. flex: 1;
  193. }
  194. :deep(.ant-steps) {
  195. .ant-steps-item {
  196. height: 80px;
  197. }
  198. .ant-steps-item-wait {
  199. .ant-steps-item-icon {
  200. background-color: initial;
  201. border-color: var(--antd-color-fill);
  202. }
  203. .ant-steps-icon {
  204. color: var(--antd-color-text-quaternary);
  205. }
  206. }
  207. .ant-steps-item-process {
  208. .ant-steps-item-title {
  209. font-weight: 500;
  210. }
  211. }
  212. .ant-steps-item-finish {
  213. .ant-steps-item-icon {
  214. background-color: initial;
  215. border-color: var(--antd-color-primary);
  216. }
  217. .ant-steps-item-title {
  218. color: var(--antd-color-text-secondary);
  219. }
  220. }
  221. .ant-steps-item-process,
  222. .ant-steps-item-wait {
  223. .ant-steps-item-tail::after {
  224. background-color: var(--antd-color-fill);
  225. }
  226. }
  227. }
  228. .doc-button {
  229. width: 120px;
  230. height: 40px;
  231. }
  232. }
  233. .use-guide-main {
  234. padding: 40px;
  235. padding-bottom: 0;
  236. overflow: auto;
  237. background: var(--antd-color-bg-base);
  238. .step-header {
  239. padding-bottom: 40px;
  240. border-bottom: 1px solid #e4e7ed;
  241. }
  242. .step-title {
  243. margin-bottom: 8px;
  244. font-size: 20px;
  245. font-weight: 500;
  246. line-height: 28px;
  247. color: var(--antd-color-text);
  248. }
  249. .step-description {
  250. min-height: 22px;
  251. font-size: 14px;
  252. line-height: 22px;
  253. color: var(--antd-color-text-secondary);
  254. }
  255. }
  256. .use-guide-footer {
  257. padding-inline: 40px;
  258. background: var(--antd-color-bg-base);
  259. }
  260. .use-guide-step-button-container {
  261. display: flex;
  262. align-items: center;
  263. justify-content: space-between;
  264. max-width: 100%;
  265. button {
  266. height: 40px;
  267. }
  268. .ant-btn-text {
  269. width: 80px;
  270. }
  271. .export-button {
  272. width: 136px;
  273. margin-right: 16px;
  274. }
  275. .next-step-button {
  276. width: 128px;
  277. font-size: 16px;
  278. }
  279. }
  280. </style>