├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── components.d.ts ├── docs ├── assets │ ├── AboutView-47a85c28.css │ ├── AboutView-edd756f1.js │ ├── AddressEdit-04ba0385.js │ ├── AddressEdit-5fb61db4.css │ ├── AddressList-a57a79bf.js │ ├── AddressList-c2108b03.css │ ├── HomeView-f739d803.js │ ├── HomeView-fd842f66.css │ ├── LuckDraw-5dee1264.css │ ├── LuckDraw-92ad1ec2.js │ ├── TodoList-bcd89d1b.js │ ├── TodoList-cac1908c.css │ ├── dayjs.min-40a0aa38.js │ ├── index-54895d77.js │ ├── index-638285ce.css │ ├── index-63a49186.js │ ├── index-7d2359eb.js │ ├── index-97037613.js │ ├── index-c09a8951.js │ ├── index-d930f8b5.js │ ├── index-dcbfa81f.css │ ├── index-f5dcfc9b.css │ ├── logo-03d6d6da.png │ ├── main-0b26ea44.css │ ├── main-fe8e16d0.js │ └── use-expose-41792aad.js └── index.html ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── src ├── App.vue ├── assets │ ├── css │ │ ├── base.css │ │ ├── chat.less │ │ └── main.less │ └── logo.png ├── components │ ├── HelloWorld.vue │ ├── Loading.ts │ ├── base │ │ ├── Button.vue │ │ ├── Select.vue │ │ └── index.ts │ └── city-picker │ │ └── index.tsx ├── composition │ └── use-rect.ts ├── directive │ ├── focus.ts │ ├── index.ts │ └── pin.ts ├── global.d.ts ├── main.ts ├── pinia │ ├── index.ts │ └── modules │ │ └── main.ts ├── router │ └── index.ts ├── utils │ ├── cookie.ts │ ├── deep-clone.ts │ ├── dom.ts │ ├── index.ts │ ├── storage.ts │ └── validate │ │ ├── date.ts │ │ ├── email.ts │ │ ├── mobile.ts │ │ ├── number.ts │ │ └── system.ts ├── views │ ├── AboutView.vue │ ├── HomeView.vue │ ├── LuckDraw.vue │ ├── TodoList.vue │ ├── address │ │ ├── AddressEdit.tsx │ │ ├── AddressList.tsx │ │ └── address.d.ts │ └── chat │ │ ├── chat.d.ts │ │ ├── index.tsx │ │ └── map-list.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue3-demo 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue3 Vite TS Demo 8 | 14 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-demo", 3 | "version": "1.2.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@vant/area-data": "^1.4.1", 13 | "dayjs": "^1.11.7", 14 | "pinia": "^2.0.36", 15 | "vant": "^4.3.1", 16 | "vue": "^3.2.47", 17 | "vue-router": "^4.2.0" 18 | }, 19 | "devDependencies": { 20 | "@vitejs/plugin-vue": "^4.1.0", 21 | "@vitejs/plugin-vue-jsx": "^3.0.1", 22 | "autoprefixer": "^10.4.14", 23 | "less": "^4.1.3", 24 | "less-loader": "^11.1.0", 25 | "postcss-pxtorem": "^6.0.0", 26 | "typescript": "^5.0.2", 27 | "unplugin-vue-components": "^0.24.1", 28 | "vite": "^4.3.2", 29 | "vue-tsc": "^1.4.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer'); 2 | const px2rem = require('postcss-pxtorem'); 3 | 4 | module.exports = { 5 | plugins: [ 6 | autoprefixer(), 7 | px2rem({ rootValue: 50, propList: ['*'] }) 8 | ] 9 | }; 10 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 32 | -------------------------------------------------------------------------------- /src/assets/css/base.css: -------------------------------------------------------------------------------- 1 | /*reset css*/ 2 | @charset "utf-8"; 3 | 4 | html,body,div,span,iframe,h1,h2,h3,h4,h5,h6,p,ol,ul,li,footer,header,menu,nav,audio,video,input { 5 | margin: 0; 6 | padding: 0; 7 | border: 0; 8 | font-weight: normal; 9 | vertical-align: baseline; 10 | -webkit-tap-highlight-color: transparent; 11 | -ms-tap-highlight-color: transparent; 12 | -moz-box-sizing: border-box; 13 | -webkit-box-sizing: border-box; 14 | box-sizing: border-box; 15 | } 16 | ::-webkit-scrollbar { 17 | display: none; 18 | } 19 | a { 20 | text-decoration: none; 21 | } 22 | li { 23 | list-style: none; 24 | } 25 | h1, h2, h3, h4, h5, h6 { 26 | font-size: 100%; 27 | font-weight: 500 28 | } 29 | /*reset css end*/ 30 | 31 | .container { 32 | position: relative; 33 | } 34 | .row { 35 | position: relative; 36 | width: 100%; 37 | } 38 | .relative { 39 | position: relative; 40 | } 41 | .text-left { 42 | text-align: left; 43 | } 44 | .text-center { 45 | text-align: center; 46 | } 47 | .text-right { 48 | text-align: right; 49 | } 50 | .pull-right { 51 | float: right !important; 52 | } 53 | .pull-left { 54 | float: left !important; 55 | } 56 | .block { 57 | display: block !important; 58 | } 59 | .inline { 60 | display: inline !important; 61 | } 62 | .inline-block { 63 | display: inline-block !important; 64 | } 65 | .clearfix:before, 66 | .clearfix:after { 67 | display: table; 68 | content: ' '; 69 | } 70 | .clearfix:after { 71 | clear: both; 72 | } 73 | 74 | /*flex*/ 75 | 76 | .flex { 77 | display: flex !important; 78 | } 79 | .flex-inline { 80 | display: inline-flex !important; 81 | } 82 | 83 | /*flex-direction*/ 84 | 85 | .flex-row { 86 | flex-direction: row; 87 | } 88 | .flex-column { 89 | flex-direction: column; 90 | } 91 | .row-reverse { 92 | flex-direction: row-reverse; 93 | } 94 | .column-reverse { 95 | flex-direction: column-reverse; 96 | } 97 | 98 | /*flex-wrap*/ 99 | 100 | .flex-wrap { 101 | flex-wrap: wrap; 102 | } 103 | .flex-nowrap { 104 | flex-wrap: nowrap; 105 | } 106 | 107 | /*justify-content*/ 108 | .space-around { 109 | justify-content: space-around; 110 | } 111 | .space-between { 112 | justify-content: space-between; 113 | } 114 | .justify-start { 115 | justify-content: flex-start; 116 | } 117 | .justify-end { 118 | justify-content: flex-end; 119 | } 120 | .justify-center { 121 | justify-content: center; 122 | } 123 | 124 | /*align-items*/ 125 | 126 | .stretch { 127 | align-items: stretch; 128 | } 129 | .align-start { 130 | align-items: flex-start; 131 | } 132 | .align-end { 133 | align-items: flex-end; 134 | } 135 | .align-middle { 136 | align-items: center; 137 | } 138 | .flex-center { 139 | justify-content: center; 140 | align-items: center; 141 | } 142 | 143 | /*order*/ 144 | 145 | .flex-first { 146 | order: -1; 147 | } 148 | .flex-last { 149 | order: 1; 150 | } 151 | 152 | /*flex*/ 153 | 154 | .flex-auto { 155 | flex: auto; 156 | } 157 | .flex-none { 158 | flex: none; 159 | } 160 | 161 | /*align-self*/ 162 | 163 | .selft-stretch { 164 | align-self: stretch; 165 | } 166 | .align-self-start { 167 | align-self: flex-start; 168 | } 169 | .align-self-end { 170 | align-self: flex-end; 171 | } 172 | .align-self-middle { 173 | align-self: center; 174 | } 175 | 176 | /*flex end*/ 177 | 178 | /*超出部分省略号*/ 179 | 180 | .txtover { 181 | overflow: hidden; 182 | white-space: nowrap; 183 | text-overflow: ellipsis; 184 | } 185 | .txtover1 { 186 | overflow: hidden; 187 | text-overflow: ellipsis; 188 | display: -webkit-box; 189 | -webkit-line-clamp: 1; 190 | -webkit-box-orient: vertical; 191 | } 192 | .txtover2 { 193 | overflow: hidden; 194 | text-overflow: ellipsis; 195 | display: -webkit-box; 196 | -webkit-line-clamp: 2; 197 | -webkit-box-orient: vertical; 198 | } 199 | .txtover3 { 200 | overflow: hidden; 201 | text-overflow: ellipsis; 202 | display: -webkit-box; 203 | -webkit-line-clamp: 3; 204 | -webkit-box-orient: vertical; 205 | } 206 | 207 | /*end*/ -------------------------------------------------------------------------------- /src/assets/css/chat.less: -------------------------------------------------------------------------------- 1 | @blue-color: #16b5eb; 2 | @green-color: #41b883; 3 | 4 | .chat-wrapper { 5 | .avatar { 6 | flex: none; 7 | margin-right: 5px; 8 | background: url('https://img.yzcdn.cn/vant/logo.png') no-repeat 0 0 / 100% 100%; 9 | width: 50px; 10 | height: 50px; 11 | border-radius: 30px; 12 | } 13 | 14 | .bubble { 15 | padding: 10px 15px; 16 | color: #333; 17 | font-size: 14px; 18 | line-height: 22px; 19 | background: #fff; 20 | border: 1px solid @green-color; 21 | border-radius: 5px; 22 | position: relative; 23 | min-width: 80px; 24 | margin: 0 15px 0 5px; 25 | 26 | img { 27 | width: 100%; 28 | } 29 | } 30 | 31 | .bubble:before { 32 | content: ''; 33 | border-style: solid; 34 | border-width: 8px; 35 | position: absolute; 36 | top: 8px; 37 | } 38 | 39 | .bubble-left:before { 40 | border-color: transparent @green-color transparent transparent; 41 | left: -17px; 42 | } 43 | 44 | .bubble-right:before { 45 | border-color: transparent transparent transparent @blue-color; 46 | right: -17px; 47 | } 48 | 49 | .bubble-right { 50 | color: #fff; 51 | background: @blue-color; 52 | border: 1px solid transparent; 53 | } 54 | } 55 | 56 | .answers-wrapper { 57 | position: fixed; 58 | bottom: 0; 59 | width: 100%; 60 | padding: 10px; 61 | background: #fff; 62 | 63 | .btnbox { 64 | flex: 0 0 50%; 65 | 66 | .btn { 67 | background: @blue-color; 68 | margin: 5px; 69 | padding: 8px 10px; 70 | border-radius: 5px; 71 | color: #fff; 72 | cursor: pointer; 73 | font-size: 12px; 74 | line-height: 18px; 75 | transition: 0.2s; 76 | width: 100%; 77 | 78 | &:active { 79 | background: #1296c3; 80 | } 81 | } 82 | } 83 | 84 | .loadingtxt { 85 | color: @blue-color; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/assets/css/main.less: -------------------------------------------------------------------------------- 1 | input { 2 | border: 1px solid #dedede; 3 | border-radius: 5px; 4 | outline: none; 5 | text-indent: 2px; 6 | height: 30px; 7 | line-height: 26px; 8 | } 9 | 10 | .mg5 { 11 | margin: 5px; 12 | } 13 | 14 | .mg10 { 15 | margin: 10px; 16 | } 17 | 18 | .mg15 { 19 | margin: 15px; 20 | } 21 | 22 | .mg20 { 23 | margin: 20px; 24 | } 25 | 26 | .mg-t0 { 27 | margin-top: 0; 28 | } 29 | 30 | .mg-r0 { 31 | margin-right: 0; 32 | } 33 | 34 | .mg-b0 { 35 | margin-bottom: 0; 36 | } 37 | 38 | .mg-l0 { 39 | margin-left: 0; 40 | } 41 | 42 | .mg-t5 { 43 | margin-top: 5px; 44 | } 45 | 46 | .mg-r5 { 47 | margin-right: 5px; 48 | } 49 | 50 | .mg-b5 { 51 | margin-bottom: 5px; 52 | } 53 | 54 | .mg-l5 { 55 | margin-left: 5px; 56 | } 57 | 58 | .mg-t10 { 59 | margin-top: 10px; 60 | } 61 | 62 | .mg-r10 { 63 | margin-right: 10px; 64 | } 65 | 66 | .mg-b10 { 67 | margin-bottom: 10px; 68 | } 69 | 70 | .mg-l10 { 71 | margin-left: 10px; 72 | } 73 | 74 | .mg-t15 { 75 | margin-top: 15px; 76 | } 77 | 78 | .mg-r15 { 79 | margin-right: 15px; 80 | } 81 | 82 | .mg-b15 { 83 | margin-bottom: 15px; 84 | } 85 | 86 | .mg-l15 { 87 | margin-left: 15px; 88 | } 89 | 90 | .mg-t20 { 91 | margin-top: 20px; 92 | } 93 | 94 | .mg-r20 { 95 | margin-right: 20px; 96 | } 97 | 98 | .mg-b20 { 99 | margin-bottom: 20px; 100 | } 101 | 102 | .mg-l20 { 103 | margin-left: 20px; 104 | } 105 | 106 | .pd5 { 107 | padding: 5px; 108 | } 109 | 110 | .pd10 { 111 | padding: 10px; 112 | } 113 | 114 | .pd15 { 115 | padding: 15px; 116 | } 117 | 118 | .pd20 { 119 | padding: 20px; 120 | } 121 | 122 | .pd-t0 { 123 | padding-top: 0; 124 | } 125 | 126 | .pd-r0 { 127 | padding-right: 0; 128 | } 129 | 130 | .pd-b0 { 131 | padding-bottom: 0; 132 | } 133 | 134 | .pd-l0 { 135 | padding-left: 0; 136 | } 137 | 138 | .pd-t5 { 139 | padding-top: 5px; 140 | } 141 | 142 | .pd-r5 { 143 | padding-right: 5px; 144 | } 145 | 146 | .pd-b5 { 147 | padding-bottom: 5px; 148 | } 149 | 150 | .pd-l5 { 151 | padding-left: 5px; 152 | } 153 | 154 | .pd-t10 { 155 | padding-top: 10px; 156 | } 157 | 158 | .pd-r10 { 159 | padding-right: 10px; 160 | } 161 | 162 | .pd-b10 { 163 | padding-bottom: 10px; 164 | } 165 | 166 | .pd-l10 { 167 | padding-left: 10px; 168 | } 169 | 170 | .pd-t15 { 171 | padding-top: 15px; 172 | } 173 | 174 | .pd-r15 { 175 | padding-right: 15px; 176 | } 177 | 178 | .pd-b15 { 179 | padding-bottom: 15px; 180 | } 181 | 182 | .pd-l15 { 183 | padding-left: 15px; 184 | } 185 | 186 | .pd-t20 { 187 | padding-top: 20px; 188 | } 189 | 190 | .pd-r20 { 191 | padding-right: 20px; 192 | } 193 | 194 | .pd-b20 { 195 | padding-bottom: 20px; 196 | } 197 | 198 | .pd-l20 { 199 | padding-left: 20px; 200 | } 201 | 202 | .fs12 { 203 | font-size: 12px; 204 | } 205 | 206 | .cred { 207 | color: red; 208 | } 209 | 210 | .pd-nav { 211 | padding-top: 46px; 212 | } 213 | 214 | .fade-enter-active, .fade-leave-active { 215 | transition: opacity 0.5s ease; 216 | } 217 | 218 | .fade-enter-from, .fade-leave-to { 219 | opacity: 0; 220 | } -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincentzyc/vue3-demo/c03534fc82d1b8d35723e2a9fd353528eb77c5d3/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Loading.ts: -------------------------------------------------------------------------------- 1 | import { Toast } from "vant"; 2 | 3 | export function openLoading(text: string): void { 4 | Toast.loading({ 5 | message: text || '', 6 | duration: 0, // 持续展示 toast 7 | forbidClick: true, // 禁用背景点击 8 | overlay: true, 9 | loadingType: 'spinner' //默认 circular 10 | }); 11 | } 12 | 13 | export function closeLoading(): void { 14 | Toast.clear() 15 | } -------------------------------------------------------------------------------- /src/components/base/Button.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/base/Select.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/base/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | import Button from './Button.vue'; 3 | import Select from './Select.vue'; 4 | 5 | const install = function (app: App): void { 6 | app.component('YuiButton', Button) 7 | app.component('YuiSelect', Select) 8 | } 9 | 10 | export default install -------------------------------------------------------------------------------- /src/components/city-picker/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref } from 'vue'; 2 | import { Popup, Area } from 'vant'; 3 | import 'vant/es/popup/style'; 4 | import 'vant/es/picker/style'; 5 | import 'vant/es/area/style'; 6 | import { areaList } from '@vant/area-data'; 7 | 8 | export default defineComponent({ 9 | props: { 10 | modelValue: { 11 | type: Array, 12 | default: () => [], 13 | }, 14 | }, 15 | setup(_props, { emit, expose }) { 16 | const showPicker = ref(false); 17 | const valuesArr = ref(['', '', '']); 18 | 19 | function confirm({ selectedOptions }: { selectedOptions: any[] }) { 20 | showPicker.value = false; 21 | valuesArr.value = selectedOptions.map(v => v.text); 22 | emit('close', valuesArr.value); 23 | emit('update:modelValue', valuesArr.value); 24 | } 25 | function cancel() { 26 | showPicker.value = false; 27 | emit('close', valuesArr.value); 28 | } 29 | 30 | function open() { 31 | showPicker.value = true; 32 | } 33 | 34 | expose({ open, close }); 35 | 36 | return () => ( 37 | 38 | 39 | 40 | ); 41 | }, 42 | }); 43 | -------------------------------------------------------------------------------- /src/composition/use-rect.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ref, unref } from 'vue'; 2 | 3 | export const useRect = (element: Element | Ref) => { 4 | return unref(element)?.getBoundingClientRect(); 5 | }; 6 | 7 | export const useHeight = (element: Element | Ref) => { 8 | const height = ref(); 9 | 10 | height.value = useRect(element)?.height || 0; 11 | 12 | return height; 13 | }; 14 | -------------------------------------------------------------------------------- /src/directive/focus.ts: -------------------------------------------------------------------------------- 1 | const focus = { 2 | mounted(el: HTMLInputElement, binding: any) { 3 | el.focus(); 4 | el.style.width = binding.value + 'px' 5 | } 6 | } 7 | 8 | export default focus -------------------------------------------------------------------------------- /src/directive/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | import focus from './focus' 3 | import pin from './pin' 4 | 5 | const install = function (app: App) { 6 | app.directive('focus', focus) 7 | app.directive('pin', pin) 8 | } 9 | 10 | export default install -------------------------------------------------------------------------------- /src/directive/pin.ts: -------------------------------------------------------------------------------- 1 | // const pin = { 2 | // mounted(el: HTMLInputElement, binding: any) { 3 | // el.style.position = 'fixed' 4 | // const s = binding.arg || 'top' 5 | // el.style[s] = binding.value + 'px' 6 | // }, 7 | // updated(el: HTMLInputElement, binding: any) { 8 | // const s = binding.arg || 'top' 9 | // el.style[s] = binding.value + 'px' 10 | // } 11 | // } 12 | 13 | const pin = (el: HTMLInputElement, binding: any) => { 14 | el.style.position = 'fixed' 15 | const s = binding.arg || 'top' 16 | el.style[s] = binding.value + 'px' 17 | } 18 | 19 | export default pin -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | scroll2Bottom(): void; 3 | } 4 | 5 | declare module '@vant/area-data' { 6 | export declare const areaList: { 7 | province_list: Record; 8 | city_list: Record; 9 | county_list: Record; 10 | }; 11 | type CascaderOption = { 12 | text: string; 13 | value: string; 14 | children?: CascaderOption[]; 15 | }; 16 | export declare function useCascaderAreaData(): CascaderOption[]; 17 | export {}; 18 | } 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import './assets/css/base.css' 5 | import './assets/css/main.less' 6 | import BaseComponents from './components/base' 7 | import BaseDirective from './directive' 8 | import { createPinia } from 'pinia' 9 | // import 'vant/lib/index.css'; 10 | import 'vant/es/dialog/style'; 11 | import 'vant/es/toast/style'; 12 | 13 | // import 'vant/es/nav-bar/style'; // 使用了 tsx 14 | 15 | 16 | const app = createApp(App) 17 | 18 | app.use(BaseComponents) 19 | app.use(BaseDirective) 20 | app.use(createPinia()) 21 | 22 | app.use(router).mount('#app') 23 | -------------------------------------------------------------------------------- /src/pinia/index.ts: -------------------------------------------------------------------------------- 1 | import useMainStore from './modules/main' 2 | 3 | export { 4 | useMainStore 5 | } -------------------------------------------------------------------------------- /src/pinia/modules/main.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | // main is the name of the store. It is unique across your application 4 | // and will appear in devtools 5 | export const useMainStore = defineStore('main', { 6 | // a function that returns a fresh state 7 | state: () => ({ 8 | counter: 0, 9 | name: 'Eduardo', 10 | selectAddress: null, 11 | undoneTodoList: [ 12 | { 13 | id: Date.now(), 14 | name: '吃饭', 15 | }, 16 | ], 17 | }), 18 | // optional getters 19 | getters: { 20 | // getters receive the state as first parameter 21 | doubleCount: state => state.counter * 2, 22 | // use getters in other getters 23 | doubleCountPlusOne(): number { 24 | return this.doubleCount * 2 + 1; 25 | }, 26 | }, 27 | // optional actions 28 | actions: { 29 | reset() { 30 | // `this` is the store instance 31 | this.counter = 0; 32 | }, 33 | }, 34 | }); 35 | 36 | export default useMainStore; 37 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: Array = [ 4 | { 5 | path: '/', 6 | name: 'Home', 7 | component: () => import(/* webpackChunkName: "home" */ '@/views/HomeView.vue') 8 | }, 9 | { 10 | path: '/about', 11 | name: 'About', 12 | // route level code-splitting 13 | // this generates a separate chunk (about.[hash].js) for this route 14 | // which is lazy-loaded when the route is visited. 15 | component: () => import(/* webpackChunkName: "about" */ '@/views/AboutView.vue') 16 | }, 17 | { 18 | path: '/todoList', 19 | name: 'TodoList', 20 | component: () => import(/* webpackChunkName: "todo" */ '@/views/TodoList.vue') 21 | }, 22 | { 23 | path: '/luckdraw', 24 | name: 'LuckDraw', 25 | component: () => import(/* webpackChunkName: "luckdraw" */ '@/views/LuckDraw.vue') 26 | }, 27 | { 28 | path: '/address/list', 29 | name: 'Address', 30 | component: () => import(/* webpackChunkName: "address" */ '@/views/address/AddressList') 31 | }, 32 | { 33 | path: '/address/edit', 34 | name: 'AddressEdit', 35 | component: () => import(/* webpackChunkName: "address" */ '@/views/address/AddressEdit') 36 | }, 37 | { 38 | path: '/chat/list', 39 | name: 'Chat', 40 | component: () => import(/* webpackChunkName: "chatpage" */ '@/views/chat') 41 | } 42 | ] 43 | 44 | const router = createRouter({ 45 | history: createWebHashHistory(), 46 | routes 47 | }) 48 | 49 | export default router 50 | -------------------------------------------------------------------------------- /src/utils/cookie.ts: -------------------------------------------------------------------------------- 1 | export function setCookie(key: string, value: string, time: number): void { 2 | const cur = new Date(); 3 | cur.setTime(cur.getTime() + time * 24 * 3600 * 1000); 4 | document.cookie = `${key}=${encodeURIComponent(value)};expires=${time ? cur.toUTCString() : ''}}` 5 | } 6 | 7 | export function getCookie(key: string): string { 8 | const data = document.cookie; 9 | let startIndex = data.indexOf(key + '='); 10 | if (startIndex > -1) { 11 | startIndex = startIndex + key.length + 1; 12 | let endIndex = data.indexOf(';', startIndex); 13 | endIndex = endIndex < 0 ? data.length : endIndex; 14 | return decodeURIComponent(data.substring(startIndex, endIndex)); 15 | } else { 16 | return ''; 17 | } 18 | } 19 | 20 | export function delCookie(key: string): void { 21 | setCookie(key, '', -1); 22 | } -------------------------------------------------------------------------------- /src/utils/deep-clone.ts: -------------------------------------------------------------------------------- 1 | import { isDef, isObject } from './index'; 2 | 3 | type ObjectIndex = Record; 4 | 5 | const { hasOwnProperty } = Object.prototype; 6 | 7 | 8 | function assignKey(to: ObjectIndex, from: ObjectIndex, key: string) { 9 | const val = from[key]; 10 | 11 | if (!isDef(val)) return; 12 | 13 | if (!hasOwnProperty.call(to, key) || !isObject(val)) { 14 | to[key] = val; 15 | } else { 16 | to[key] = deepAssign(Object(to[key]), from[key]); 17 | } 18 | } 19 | 20 | function deepAssign(to: ObjectIndex, from: ObjectIndex): ObjectIndex { 21 | Object.keys(from).forEach((key) => { 22 | assignKey(to, from, key); 23 | }); 24 | 25 | return to; 26 | } 27 | 28 | 29 | export function deepClone(obj: Record): Record { 30 | if (Array.isArray(obj)) { 31 | return obj.map((item) => deepClone(item)); 32 | } 33 | 34 | if (typeof obj === 'object') { 35 | return deepAssign({}, obj); 36 | } 37 | 38 | return obj; 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 滑动到底部 3 | */ 4 | export function easeBottom() { 5 | let position = window.pageYOffset 6 | const destination = document.documentElement.offsetHeight - document.documentElement.clientHeight 7 | // 不存在原生`requestAnimationFrame`,用`setTimeout`模拟替代 8 | if (!window.requestAnimationFrame) { 9 | window.requestAnimationFrame = function (fn) { 10 | return setTimeout(fn, 17); 11 | }; 12 | } 13 | function step() { 14 | position = position + (destination - position) / 8; 15 | if (Math.abs(destination - position) < 2) { 16 | //动画结束 17 | window.scrollTo(0, destination) 18 | return; 19 | } 20 | window.scrollTo(0, position) 21 | requestAnimationFrame(step); 22 | } 23 | step(); 24 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | type StringObj = { [k: string]: string } 2 | type UrlParamBack = null | string | StringObj 3 | 4 | /** 5 | * 获取数据类型 6 | * @param {any} value 需要判断的值 7 | * @return "String","Object","Array"... 8 | */ 9 | export function getType(value: any) { 10 | return Object.prototype.toString.call(value).slice(8, -1) 11 | } 12 | 13 | export function isDef(val: unknown): boolean { 14 | return val !== undefined && val !== null; 15 | } 16 | 17 | export function isObject(val: unknown): val is Record { 18 | return Object.prototype.toString.call(val).slice(8, -1) === 'Object'; 19 | } 20 | 21 | export function isArray(val: unknown): boolean { 22 | return Object.prototype.toString.call(val).slice(8, -1) === 'Array'; 23 | } 24 | 25 | export function isString(val: unknown): boolean { 26 | return Object.prototype.toString.call(val).slice(8, -1) === 'String'; 27 | } 28 | 29 | /** 30 | * 获取url参数值 31 | * @param {String} name 参数名称(不传则返回一个全部参数对象) 32 | */ 33 | export function getUrlParam(name = ''): UrlParamBack { 34 | const href = window.location.href, i = href.indexOf("?"); 35 | if (i < 0) return null; 36 | const str = href.slice(i); 37 | if (!str) return null; 38 | const arr = str.match(/([^?&=#]+)=([^?&=#/]*)/g); 39 | if (!arr) return null; 40 | const obj: StringObj = {} 41 | arr.forEach(v => { 42 | const temp = v.split('='); 43 | if (temp.length > 0) { 44 | obj[temp[0]] = temp[1]; 45 | } 46 | }) 47 | if (name) return obj[name] 48 | return obj 49 | } -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | export function setSessionStorage(key: string, value: any): void { 2 | const str = window.JSON.stringify(value); 3 | window.sessionStorage.setItem(key, str); 4 | } 5 | 6 | export function getSessionStorage(key: string): any { 7 | let json: string | null; 8 | json = window.sessionStorage.getItem(key); 9 | if (json) json = window.JSON.parse(json); 10 | return json; 11 | } 12 | 13 | export function setLocalStorage(key: string, value: any): void { 14 | const str = window.JSON.stringify(value); 15 | window.localStorage.setItem(key, str); 16 | } 17 | 18 | export function getLocalStorage(key: string): any { 19 | let json: string | null; 20 | json = window.localStorage.getItem(key); 21 | if (json) json = window.JSON.parse(json); 22 | return json; 23 | } -------------------------------------------------------------------------------- /src/utils/validate/date.ts: -------------------------------------------------------------------------------- 1 | import { isNaN } from './number'; 2 | import { getType } from '../index' 3 | 4 | export function isDate(val: Date): val is Date { 5 | return ( 6 | getType(val) === 'Date' && 7 | !isNaN(val.getTime()) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/validate/email.ts: -------------------------------------------------------------------------------- 1 | export function isEmail(value: string): boolean { 2 | const reg = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 3 | return reg.test(value); 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/validate/mobile.ts: -------------------------------------------------------------------------------- 1 | export function isMobile(value: string): boolean { 2 | value = value.replace(/[^-|\d]/g, ''); 3 | return ( 4 | /^((\+86)|(86))?(1)\d{10}$/.test(value) || /^0[0-9-]{10,13}$/.test(value) 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/validate/number.ts: -------------------------------------------------------------------------------- 1 | export function isNumeric(val: string): boolean { 2 | return /^\d+(\.\d+)?$/.test(val); 3 | } 4 | 5 | export function isNaN(val: number): val is typeof NaN { 6 | if (Number.isNaN) { 7 | return Number.isNaN(val); 8 | } 9 | 10 | // eslint-disable-next-line no-self-compare 11 | return val !== val; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/validate/system.ts: -------------------------------------------------------------------------------- 1 | export const inBrowser = typeof window !== 'undefined' 2 | 3 | export function isAndroid(): boolean { 4 | /* istanbul ignore next */ 5 | return inBrowser ? /android/.test(navigator.userAgent.toLowerCase()) : false; 6 | } 7 | 8 | export function isIOS(): boolean { 9 | /* istanbul ignore next */ 10 | return inBrowser 11 | ? /ios|iphone|ipad|ipod/.test(navigator.userAgent.toLowerCase()) 12 | : false; 13 | } 14 | -------------------------------------------------------------------------------- /src/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | -------------------------------------------------------------------------------- /src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 77 | 78 | 83 | -------------------------------------------------------------------------------- /src/views/LuckDraw.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 236 | 237 | 384 | -------------------------------------------------------------------------------- /src/views/TodoList.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | -------------------------------------------------------------------------------- /src/views/address/AddressEdit.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref, reactive, computed, onUnmounted } from 'vue'; 2 | import { useRouter } from 'vue-router'; 3 | import { Form, Field, NavBar, Button, Dialog, Cell, Switch, showToast } from 'vant'; 4 | import { openLoading, closeLoading } from '@/components/Loading'; 5 | import { getLocalStorage, setLocalStorage } from '@/utils/storage'; 6 | import { AddressInfo } from './address'; 7 | import CityPicker from '@/components/city-picker'; 8 | import { useMainStore } from '@/pinia'; 9 | import 'vant/es/form/style'; 10 | import 'vant/es/field/style'; 11 | import 'vant/es/nav-bar/style'; 12 | import 'vant/es/button/style'; 13 | import 'vant/es/cell/style'; 14 | import 'vant/es/switch/style'; 15 | 16 | export default defineComponent({ 17 | setup() { 18 | const router = useRouter(); 19 | const mainStore = useMainStore(); 20 | 21 | const cityPicker = ref(); 22 | const vanForm = ref(); 23 | 24 | let form = reactive({ 25 | id: Date.now(), 26 | name: '', 27 | tel: '', 28 | address: '', 29 | ads: '', 30 | city: [], 31 | isDefault: false, 32 | }); 33 | 34 | const patterns = { 35 | phone: /^1[0-9]{10}$/, 36 | name: /^[\u4e00-\u9fa5]{2,20}$/, 37 | ads: /^[\u4E00-\u9FA5A-Za-z0-9_—()()-]+$/, 38 | }; 39 | 40 | const messages = { 41 | phone: (val: string) => { 42 | if (val === '') return '请输入手机号'; 43 | return '手机号不正确'; 44 | }, 45 | name: (val: string) => { 46 | if (val === '') return '请输入姓名'; 47 | return '姓名输入有误'; 48 | }, 49 | ads: (val: string) => { 50 | if (val === '') return '请输入详细地址'; 51 | return '详细地址输入有误'; 52 | }, 53 | }; 54 | 55 | const cityValue = computed(() => { 56 | if (form.city.length > 0) return form.city.join(' '); 57 | return ''; 58 | }); 59 | 60 | const openCity = () => { 61 | cityPicker.value.open(); 62 | }; 63 | 64 | const routerBack = () => { 65 | router.back(); 66 | }; 67 | 68 | const onSubmit = () => { 69 | vanForm.value 70 | .validate() 71 | .then(() => { 72 | openLoading('正在保存'); 73 | console.log('submit', form); 74 | let addressList = getLocalStorage('addressList'); 75 | if (addressList && Array.isArray(addressList)) { 76 | form.address = form.city.join('') + form.ads; 77 | const index = addressList.findIndex(v => { 78 | return v.id === form.id; 79 | }); 80 | if (form.isDefault) { 81 | addressList.forEach(v => (v.isDefault = false)); 82 | } 83 | if (index >= 0) { 84 | addressList.splice(index, 1, form); 85 | } else { 86 | addressList.push(form); 87 | } 88 | } else { 89 | addressList = [form]; 90 | } 91 | setLocalStorage('addressList', addressList); 92 | setTimeout(() => { 93 | closeLoading(); 94 | showToast('保存成功'); 95 | routerBack(); 96 | }, 1000); 97 | }) 98 | .catch((err: []) => { 99 | console.log(err); 100 | }); 101 | }; 102 | 103 | const handleDelete = () => { 104 | Dialog.confirm({ 105 | title: '提示', 106 | message: '确定删除此地址?', 107 | }) 108 | .then(() => { 109 | // on confirm 110 | let localAddress = getLocalStorage('addressList'); 111 | localAddress = localAddress?.filter((v: AddressInfo) => v.id !== form.id); 112 | if (form.isDefault) { 113 | if (Array.isArray(localAddress) && localAddress.length > 0) localAddress[0].isDefault = true; 114 | } 115 | setLocalStorage('addressList', localAddress); 116 | showToast('删除成功'); 117 | router.back(); 118 | }) 119 | .catch(() => { 120 | // on cancel 121 | }); 122 | }; 123 | 124 | const SwitchSlots = { 125 | 'right-icon': () => , 126 | }; 127 | 128 | const initAddress = () => { 129 | if (mainStore.selectAddress) form = mainStore.selectAddress; 130 | }; 131 | initAddress(); 132 | 133 | onUnmounted(() => { 134 | mainStore.selectAddress = null; 135 | }); 136 | 137 | return () => ( 138 |
139 | 140 |
141 |
142 | 148 | 156 | 165 | 171 | 172 | {' '} 173 | 174 |
175 | 178 | 181 |
182 | 183 |
184 | 185 | 186 |
187 | ); 188 | }, 189 | }); 190 | -------------------------------------------------------------------------------- /src/views/address/AddressList.tsx: -------------------------------------------------------------------------------- 1 | import { ref, reactive, defineComponent } from "vue"; 2 | import { useRouter } from "vue-router"; 3 | import { AddressList, NavBar } from "vant"; 4 | import { getLocalStorage, setLocalStorage } from '@/utils/storage'; 5 | import { AddressInfo } from "./address" 6 | import { useMainStore } from '@/pinia'; 7 | import 'vant/es/nav-bar/style'; 8 | import 'vant/es/address-list/style'; 9 | 10 | export default defineComponent(() => { 11 | const router = useRouter() 12 | const mainStore = useMainStore(); 13 | const chosenAddressId = ref() 14 | const localAddress = getLocalStorage('addressList') 15 | 16 | const list: Array = reactive(localAddress || [ 17 | { 18 | id: 1, 19 | name: '张三', 20 | tel: '13012345678', 21 | address: '浙江省杭州市西湖区文三路138号东方通信大厦7楼501室', 22 | ads: "文三路 138 号东方通信大厦7楼501室", 23 | city: ["浙江省", "杭州市", "西湖区"], 24 | isDefault: true, 25 | }, 26 | { 27 | id: 2, 28 | name: '李四', 29 | tel: '13112345678', 30 | address: '浙江省杭州市拱墅区莫干山路50号', 31 | ads: "莫干山路 50 号", 32 | city: ["浙江省", "杭州市", "拱墅区"], 33 | isDefault: false, 34 | }, 35 | { 36 | id: 3, 37 | name: '王五', 38 | tel: '13212345678', 39 | address: '浙江省杭州市滨江区江南大道15号', 40 | ads: "江南大道 15 号", 41 | city: ["浙江省", "杭州市", "滨江区"], 42 | isDefault: false, 43 | } 44 | ]) 45 | 46 | if (!localAddress) setLocalStorage('addressList', list) 47 | 48 | const onAdd = () => { 49 | router.push('/address/edit') 50 | } 51 | const onEdit = (item: any) => { 52 | mainStore.selectAddress = item 53 | router.push('/address/edit') 54 | } 55 | 56 | const onClickLeft = () => { 57 | router.back() 58 | } 59 | 60 | return () => { 61 | return ( 62 |
63 | 64 | 71 |
72 | ); 73 | } 74 | }) -------------------------------------------------------------------------------- /src/views/address/address.d.ts: -------------------------------------------------------------------------------- 1 | export interface AddressInfo { 2 | id: number; 3 | name: string; 4 | tel: string; 5 | address: string; 6 | ads: string; 7 | city: [string, string, string]; 8 | isDefault: boolean 9 | } -------------------------------------------------------------------------------- /src/views/chat/chat.d.ts: -------------------------------------------------------------------------------- 1 | export interface QuestionList { 2 | [k: string]: string 3 | } 4 | 5 | export interface AnswerList { 6 | [k: string]: string 7 | } 8 | 9 | export interface MapList { 10 | [k: string]: string | { 11 | [k: string]: string 12 | } 13 | } 14 | 15 | export interface MsgList { 16 | message: string 17 | direction: string 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/views/chat/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref, reactive, nextTick, onMounted, onUnmounted } from "vue"; 2 | import { useRouter } from "vue-router"; 3 | import { NavBar } from "vant"; 4 | import { questionList, answerList, mapList } from "./map-list" 5 | import "@/assets/css/chat.less" 6 | import { easeBottom } from '@/utils/dom'; 7 | import { MsgList } from "./chat" 8 | import { useHeight } from '@/composition/use-rect'; 9 | 10 | export default defineComponent(() => { 11 | const router = useRouter() 12 | const answerRef = ref(); 13 | const msgList: Array = reactive([]) 14 | const chatBottomHeight = useHeight(answerRef); 15 | const showLoading = ref(true) 16 | const loadingText = ref('对方正在输入中...') 17 | 18 | let timer = 0 19 | let loadingTimer = 0 20 | let ansList: Array = reactive([]) 21 | let answers = mapList.Q1 22 | 23 | window.scroll2Bottom = () => easeBottom() 24 | 25 | const getDelayTime = () => Math.ceil((Math.random() * 2 + 1.5) * 1000) //1.5-3.5秒 26 | const getLoadingDelayTime = () => Math.ceil(Math.random() + 0.5 * 1000) //0.5-1.5秒 27 | 28 | const onClickLeft = () => { 29 | router.back() 30 | } 31 | 32 | const scrollPageBottom = async () => { 33 | await nextTick() 34 | chatBottomHeight.value = useHeight(answerRef).value; 35 | window.scroll2Bottom() 36 | } 37 | 38 | const chatFinish = () => { 39 | console.log('聊天结束'); 40 | showLoading.value = false 41 | } 42 | 43 | const setMsg = (message: string, direction: 'left' | 'right') => { 44 | msgList.push({ 45 | message: message, 46 | direction: direction 47 | }) 48 | } 49 | 50 | const setQuestion = (question: string) => { 51 | setMsg(questionList[question], 'left') 52 | answers = mapList[question] 53 | if (!answers) return chatFinish() 54 | if (typeof answers === 'string') { 55 | timer = setTimeout(() => { 56 | setQuestion(answers as string) 57 | scrollPageBottom() 58 | }, getDelayTime()); 59 | } else { 60 | ansList = Object.keys(answers) 61 | } 62 | } 63 | 64 | const addAnswer = (item: string) => { 65 | setMsg(answerList[item], 'right') 66 | ansList = [] 67 | loadingText.value = '请稍等...' 68 | loadingTimer = setTimeout(() => { 69 | loadingText.value = '对方正在输入中...' 70 | }, getLoadingDelayTime()); 71 | timer = setTimeout(() => { 72 | setQuestion(typeof answers === 'string' ? answers : answers[item]) 73 | scrollPageBottom() 74 | }, getDelayTime()); 75 | } 76 | 77 | const messageDom = () => msgList.length > 0 && ( 78 |
79 | {msgList.map(item => ( 80 |
  • 81 | {item.direction === 'left' ?
    : null} 82 |
    83 |

    84 |
    85 | {item.direction === 'right' ?
    : null} 86 |
  • 87 | ))} 88 |
    ) 89 | 90 | const answerDom = () => ansList.length > 0 ? ( 91 |
    92 | {ansList.map((item: string) => ( 93 |
    94 |
    addAnswer(item)}>{answerList[item]}
    95 |
    96 | ))} 97 |
    98 | ) : ( 99 |
    100 |
    {loadingText.value}
    101 |
    102 | ) 103 | 104 | onMounted(() => { 105 | setQuestion('Q1') 106 | }) 107 | onUnmounted(() => { 108 | clearTimeout(timer) 109 | clearTimeout(loadingTimer) 110 | }) 111 | 112 | return () => ( 113 |
    114 | 115 | {messageDom()} 116 | {answerDom()} 117 |
    118 | ) 119 | }) -------------------------------------------------------------------------------- /src/views/chat/map-list.ts: -------------------------------------------------------------------------------- 1 | import { QuestionList, AnswerList, MapList } from "./chat" 2 | 3 | export const questionList: QuestionList = { 4 | Q1: '

    这是第1个问题哈哈自定义html

    ', 5 | Q2: '图片图片', 6 | Q3: '这是第3个问题这是第3个问题这是第3个问题', 7 | Q4: '这是第4个这这是第4个问题题问题', 8 | Q5: '

    这是第5个问题

    ipad', 9 | Q6: '这是第6个问第7个题', 10 | Q7: '这是第7个问题第7个第7个', 11 | Q8: '问题结束', 12 | Q9: '结束语', 13 | } 14 | 15 | export const answerList: AnswerList = { 16 | A1: '这是第1个回答选项', 17 | A2: '这是第2个答选项回答选项', 18 | A3: '这是第3个回答选项', 19 | A4: '这是第4个回答选项答', 20 | A5: '这答选项是第5个回答选项答选项', 21 | A6: '这是第6个回答选项', 22 | A7: '这是第7个回答选项这是第7个回答选项', 23 | A8: '这是第8个回答选项', 24 | A9: '这是第9个回答选项答选项', 25 | A10: '这是第10个回答选项', 26 | A11: '这是第11个回答选项答选项答选项', 27 | A12: '这是第12个回答选项', 28 | A13: '这是第13个回答选项答选项答选项答选项', 29 | A14: '这是第14个回答选项', 30 | A15: '这是第15个回答选项答选项', 31 | A16: '这是第16个回答选项' 32 | } 33 | 34 | export const mapList: MapList = { 35 | Q1: { 36 | A1: 'Q6', 37 | A2: 'Q3', 38 | A3: 'Q7', 39 | A4: 'Q5', 40 | }, 41 | Q2: { 42 | A5: 'Q4', 43 | A6: 'Q3', 44 | A7: 'Q8', 45 | A8: 'Q1', 46 | }, 47 | Q3: { 48 | A9: 'Q2', 49 | A10: 'Q4', 50 | A11: 'Q8', 51 | }, 52 | Q4: { 53 | A5: 'Q1', 54 | A7: 'Q8', 55 | A2: 'Q3', 56 | A8: 'Q4', 57 | }, 58 | Q5: { 59 | A1: 'Q5', 60 | A3: 'Q6', 61 | A9: 'Q7', 62 | A11: 'Q8', 63 | }, 64 | Q6: { 65 | A4: 'Q2', 66 | A6: 'Q6', 67 | A10: 'Q4', 68 | A12: 'Q7', 69 | }, 70 | Q7: 'Q8', 71 | Q8: 'Q9' 72 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "skipLibCheck": true, 9 | "paths": { 10 | "@/*": ["src/*"] 11 | }, 12 | 13 | /* Bundler mode */ 14 | "moduleResolution": "bundler", 15 | "allowImportingTsExtensions": true, 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "preserve", 20 | 21 | /* Linting */ 22 | "strict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noFallthroughCasesInSwitch": true 26 | }, 27 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 28 | "references": [{ "path": "./tsconfig.node.json" }] 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import Components from 'unplugin-vue-components/vite'; 4 | import { VantResolver } from 'unplugin-vue-components/resolvers'; 5 | import vueJsx from '@vitejs/plugin-vue-jsx'; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | base: './', 10 | server: { 11 | host: true, 12 | port: 8010, 13 | }, 14 | build: { 15 | outDir: 'docs', 16 | }, 17 | plugins: [ 18 | vue(), 19 | vueJsx(), 20 | Components({ 21 | resolvers: [VantResolver()], 22 | }), 23 | ], 24 | resolve: { 25 | alias: { 26 | '@': '/src', 27 | }, 28 | }, 29 | }); 30 | --------------------------------------------------------------------------------