UseGuidance.vue 6.5 KB

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