├── .env.development ├── .env.production ├── .gitignore ├── .prettierrc ├── .vscode └── extensions.json ├── README.md ├── index.html ├── package.json ├── postcss.config.js ├── public └── vite.svg ├── src ├── App.vue ├── api │ ├── category.js │ ├── list.js │ ├── pay.js │ └── sys.js ├── assets │ ├── icons │ │ ├── back.svg │ │ ├── calendar.svg │ │ ├── change-header-image.svg │ │ ├── close.svg │ │ ├── comment.svg │ │ ├── countdown.svg │ │ ├── delete.svg │ │ ├── down-arrow.svg │ │ ├── download.svg │ │ ├── error.svg │ │ ├── feedback.svg │ │ ├── fine-loading.svg │ │ ├── fold.svg │ │ ├── full.svg │ │ ├── guide.svg │ │ ├── hamburger.svg │ │ ├── heart.svg │ │ ├── home.svg │ │ ├── infinite-load.svg │ │ ├── input-delete.svg │ │ ├── like.svg │ │ ├── loading.svg │ │ ├── logout.svg │ │ ├── pay-fail.svg │ │ ├── pay-success.svg │ │ ├── profile.svg │ │ ├── qq.svg │ │ ├── read.svg │ │ ├── refresh.svg │ │ ├── remind.svg │ │ ├── search.svg │ │ ├── setting.svg │ │ ├── share.svg │ │ ├── success.svg │ │ ├── theme-dark.svg │ │ ├── theme-light.svg │ │ ├── theme-system.svg │ │ ├── theme.svg │ │ ├── unfold.svg │ │ ├── vip-profile.svg │ │ ├── vip.svg │ │ ├── warn.svg │ │ ├── wei-bo.svg │ │ ├── wei-xin.svg │ │ ├── wexin.svg │ │ └── zhi-fu-bao.svg │ ├── images │ │ └── alipay.png │ └── vue.svg ├── components │ └── HelloWorld.vue ├── constants │ └── index.js ├── directives │ ├── index.js │ └── modules │ │ └── lazy.js ├── libs │ ├── button │ │ └── index.vue │ ├── confirm │ │ ├── index.js │ │ └── index.vue │ ├── count-down │ │ ├── index.vue │ │ └── utils.js │ ├── dialog │ │ └── index.vue │ ├── index.js │ ├── infinite-list │ │ └── index.vue │ ├── input │ │ └── index.vue │ ├── message │ │ ├── index.js │ │ └── index.vue │ ├── navbar │ │ └── index.vue │ ├── popover │ │ └── index.vue │ ├── popup │ │ └── index.vue │ ├── search │ │ └── index.vue │ ├── svg-icon │ │ └── index.vue │ ├── transition-router-view │ │ └── index.vue │ ├── trigger-menu-item │ │ └── index.vue │ ├── trigger-menu │ │ └── index.vue │ └── waterfall │ │ ├── index.vue │ │ └── utils.js ├── main.js ├── permission.js ├── router │ ├── index.js │ └── modules │ │ ├── mobile-routes.js │ │ └── pc-routes.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── category.js │ │ ├── search.js │ │ ├── theme.js │ │ └── user.js ├── style.css ├── styles │ └── index.scss ├── utils │ ├── color.js │ ├── flexible.js │ ├── index.js │ ├── request.js │ ├── sts.js │ └── theme.js ├── vendor │ └── SliderCaptcha │ │ ├── longbow.slidercaptcha.min.js │ │ └── slidercaptcha.min.css └── views │ ├── layout │ ├── components │ │ ├── floating │ │ │ ├── index.vue │ │ │ └── steps.js │ │ ├── header │ │ │ ├── header-my.vue │ │ │ ├── header-search │ │ │ │ ├── hint.vue │ │ │ │ ├── history.vue │ │ │ │ ├── hot.vue │ │ │ │ └── index.vue │ │ │ ├── header-theme.vue │ │ │ └── index.vue │ │ └── main │ │ │ └── index.vue │ └── index.vue │ ├── login │ ├── components │ │ ├── header.vue │ │ ├── qq-login.vue │ │ ├── slider-captcha.vue │ │ └── wechat-login.vue │ ├── index.vue │ └── validate.js │ ├── main │ ├── components │ │ ├── list │ │ │ ├── index.vue │ │ │ └── item.vue │ │ ├── menu │ │ │ └── index.vue │ │ └── navigation │ │ │ ├── index.vue │ │ │ ├── mobile │ │ │ └── index.vue │ │ │ └── pc │ │ │ └── index.vue │ └── index.vue │ ├── member │ ├── components │ │ ├── pay-menu-item.vue │ │ └── payment │ │ │ ├── discounts.vue │ │ │ ├── index.vue │ │ │ ├── mobile-payment │ │ │ ├── index.vue │ │ │ └── mobile-pay-select.vue │ │ │ └── pc-payment │ │ │ └── index.vue │ └── index.vue │ ├── profile │ ├── components │ │ └── change-avatar.vue │ └── index.vue │ └── register │ └── index.vue ├── tailwind.config.js ├── vite.config.js └── yarn.lock /.env.development: -------------------------------------------------------------------------------- 1 | VITE_BASE_API = "/api" -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_BASE_API = "/prod-api" -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "none" 5 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vite + Vue 9 | 10 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hm", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@vueuse/core": "^10.1.2", 13 | "ali-oss": "^6.17.1", 14 | "axios": "^1.4.0", 15 | "cropperjs": "^1.5.13", 16 | "dayjs": "^1.11.9", 17 | "driver.js": "^0.9.8", 18 | "file-saver": "^2.0.5", 19 | "md5": "^2.3.0", 20 | "vee-validate": "^4.10.2", 21 | "vue": "^3.2.47", 22 | "vue-router": "^4.2.2", 23 | "vuex": "^4.1.0", 24 | "vuex-persistedstate": "^4.1.0" 25 | }, 26 | "devDependencies": { 27 | "@vitejs/plugin-vue": "^4.1.0", 28 | "autoprefixer": "^10.4.14", 29 | "postcss": "^8.4.24", 30 | "sass": "^1.62.1", 31 | "tailwindcss": "^3.3.2", 32 | "vite": "^4.3.9", 33 | "vite-plugin-svg-icons": "^2.0.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/api/category.js: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request" 2 | 3 | // 获取全部分类数据 4 | export const getCategoryList = () => { 5 | return request({ 6 | method: "get", 7 | url: "/category" 8 | }) 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/api/list.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 请求列表数据 5 | */ 6 | 7 | export default function getList(data) { 8 | return request({ 9 | method: 'get', 10 | url: '/pexels/list', 11 | params: data 12 | }) 13 | } 14 | 15 | /** 16 | * 获取搜索关键词列表数据 17 | */ 18 | 19 | export const getHint = (q) => { 20 | return request({ 21 | url: '/pexels/hint', 22 | params: { 23 | q 24 | } 25 | }) 26 | } 27 | 28 | /** 29 | * 获取推荐主题 30 | */ 31 | export const getHotThemes = () => { 32 | return request({ 33 | url: '/pexels/themes' 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/api/pay.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 获取 VIP 支付数据 5 | */ 6 | export const getVipPayList = () => { 7 | return request({ 8 | url: '/user/vip/pay/list' 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /src/api/sys.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 人类行为验证接口 5 | */ 6 | export function getCaptcha(data) { 7 | return request({ 8 | url: '/sys/captcha', 9 | method: 'POST', 10 | data 11 | }) 12 | } 13 | 14 | /** 15 | * 登录接口 16 | */ 17 | 18 | export function postLogin(data) { 19 | return request({ 20 | url: '/sys/login', 21 | method: 'post', 22 | data 23 | }) 24 | } 25 | 26 | /** 27 | * 获取用户信息 28 | */ 29 | export const getProfile = (data) => { 30 | return request({ 31 | url: '/user/profile', 32 | data 33 | }) 34 | } 35 | 36 | /** 37 | * 注册 38 | */ 39 | export const postRegister = (data) => { 40 | return request({ 41 | url: '/sys/register', 42 | method: 'POST', 43 | data 44 | }) 45 | } 46 | 47 | /** 48 | * 修改用户信息 49 | */ 50 | export const putProfile = (data) => { 51 | return request({ 52 | url: '/user/profile', 53 | method: 'PUT', 54 | data 55 | }) 56 | } 57 | 58 | /** 59 | * 获取 OSS 上传凭证 60 | * 61 | * 获取oss一些秘钥参数 62 | */ 63 | export const getSts = () => { 64 | return request({ 65 | url: '/user/sts' 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /src/assets/icons/back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/calendar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/change-header-image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/comment.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/countdown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/down-arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/feedback.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/fine-loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/fold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/full.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/hamburger.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/heart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/infinite-load.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/input-delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/like.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/logout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/pay-fail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/pay-success.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/profile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/qq.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/read.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/remind.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/setting.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/share.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/success.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/theme-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/theme-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/theme-system.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/unfold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/vip-profile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/vip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/warn.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/wei-bo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/wei-xin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/wexin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/zhi-fu-bao.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhang-glitch/work_technology_solutions/9f2f0e9bc573d98329a4b6464a944e3599447be5/src/assets/images/alipay.png -------------------------------------------------------------------------------- /src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 35 | 36 | 41 | -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const PC_DEVICE_WIDTH = 1280 2 | 3 | // 全部分类 4 | export const ALL_CATEGORY = { id: 'all', name: '全部' } 5 | 6 | // 暗黑主题 7 | export const THEME_DARK = 'dark' 8 | // 浅色主题 9 | export const THEME_LIGHT = 'light' 10 | // 系统主题 11 | export const THEME_SYSTEM = 'system' 12 | 13 | // 初始 category 数据 14 | export const CATEGORY_NOMAR_DATA = [ 15 | ALL_CATEGORY, 16 | { id: 'web_app_icon', name: 'UI/UX' }, 17 | { id: 'design', name: '平面' }, 18 | { id: 'illustration', name: '插画/漫画' }, 19 | { id: 'photography', name: '摄影' }, 20 | { id: 'games', name: '游戏' }, 21 | { id: 'anime', name: '动漫' }, 22 | { 23 | id: 'industrial_design', 24 | name: '工业设计' 25 | }, 26 | { 27 | id: 'industrial_design', 28 | name: '建筑设计' 29 | }, 30 | { 31 | id: 'industrial_design', 32 | name: '人文艺术' 33 | }, 34 | { 35 | id: 'industrial_design', 36 | name: '家居/家装' 37 | } 38 | ] 39 | 40 | // STS 上传数据 41 | export const REGION = 'oss-cn-beijing' 42 | export const BUCKET = 'imooc-front' 43 | -------------------------------------------------------------------------------- /src/directives/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | install(app) { 3 | // 获取到所有指令 4 | const modules = import.meta.globEager('./modules/*.js') 5 | for (let [key, value] of Object.entries(modules)) { 6 | const directiveName = key.replace('./modules/', '').split('.')[0] 7 | app.directive(directiveName, value.default) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/directives/modules/lazy.js: -------------------------------------------------------------------------------- 1 | import { useIntersectionObserver } from '@vueuse/core' 2 | 3 | export default { 4 | mounted(el) { 5 | // 保存图片路径 6 | const imgSrc = el.getAttribute('src') 7 | // 将图片src置空 8 | el.setAttribute('src', '') 9 | 10 | // 监听图片的可见 11 | const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => { 12 | if (isIntersecting) { 13 | el.setAttribute('src', imgSrc) 14 | // 停止监听 15 | stop() 16 | } 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/libs/button/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 61 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/libs/confirm/index.js: -------------------------------------------------------------------------------- 1 | import { h, render } from 'vue' 2 | import Confirm from './index.vue' 3 | 4 | export function createConfirm({ 5 | title, 6 | content, 7 | cancelText = '取消', 8 | confirmText = '确定' 9 | }) { 10 | return new Promise((resolve, reject) => { 11 | // 暂时性死区 12 | // const confirmInstance = createApp(Confirm, { 13 | // title, 14 | // content, 15 | // cancelText, 16 | // confirmText, 17 | // closeAfter, 18 | // handleConfirmClick, 19 | // handleCancelClick 20 | // }) 21 | // const mountNode = document.createElement('div') 22 | // document.body.appendChild(mountNode) 23 | // confirmInstance.mount(mountNode) 24 | 25 | /** 26 | * 移除confirm 27 | */ 28 | const closeAfter = () => { 29 | // const confirmId = document.getElementById('confirm') 30 | // confirmId && document.body.removeChild(confirmId) 31 | 32 | // 2. render 33 | render(null, document.body) 34 | } 35 | 36 | /** 37 | * 点击确定按钮,回调 38 | */ 39 | const handleConfirmClick = resolve 40 | 41 | /** 42 | * 点击取消按钮,回调 43 | */ 44 | const handleCancelClick = reject 45 | 46 | // 1. vnode 生成vnode,并传入props 47 | const vnode = h(Confirm, { 48 | title, 49 | content, 50 | cancelText, 51 | confirmText, 52 | closeAfter, 53 | handleConfirmClick, 54 | handleCancelClick 55 | }) 56 | // 2. render 渲染组件到body中 57 | render(vnode, document.body) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /src/libs/confirm/index.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 125 | 126 | 153 | -------------------------------------------------------------------------------- /src/libs/count-down/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/libs/count-down/utils.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import 'dayjs/locale/zh-cn' 3 | import duration from 'dayjs/plugin/duration' 4 | 5 | dayjs.locale('zh') 6 | dayjs.extend(duration) 7 | 8 | export default dayjs 9 | -------------------------------------------------------------------------------- /src/libs/dialog/index.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 105 | 106 | 128 | -------------------------------------------------------------------------------- /src/libs/index.js: -------------------------------------------------------------------------------- 1 | // import SvgIcon from './svg-icon/index.vue' 2 | // import HmPopup from './popup/index.vue' 3 | 4 | import { defineAsyncComponent } from 'vue' 5 | // 导出函数组件 6 | export { createConfirm } from './confirm/index' 7 | export { createMessage } from './message/index' 8 | 9 | // const components = [SvgIcon, HmPopup] 10 | 11 | export default { 12 | install(app) { 13 | // components.forEach((element) => { 14 | // app.component(element, element) 15 | // }) 16 | // 获取当前路径下所有文件夹下的index.vue 17 | const components = import.meta.glob('./*/index.vue') 18 | // 遍历获取到的组件模块 19 | for (let [key, component] of Object.entries(components)) { 20 | const componentName = 'hm-' + key.replace('./', '').split('/')[0] 21 | // 通过 defineAsyncComponent 异步导入指定路径下的组件 22 | app.component(componentName, defineAsyncComponent(component)) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/libs/infinite-list/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 25 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/libs/input/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 30 | 31 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/libs/message/index.js: -------------------------------------------------------------------------------- 1 | import { createApp, h, render } from 'vue' 2 | import Message from './index.vue' 3 | 4 | export function createMessage({ type, content, delay = 3000 }) { 5 | // const messageInstance = createApp(Message, { 6 | // type, 7 | // content, 8 | // delay 9 | // }) 10 | 11 | // // 挂载 12 | // const mountNode = document.createElement('div') 13 | // document.body.appendChild(mountNode) 14 | // messageInstance.mount(mountNode) 15 | // // 指定时间内,移除message组件 16 | // setTimeout(() => { 17 | // messageInstance.unmount(mountNode) 18 | // document.body.removeChild(mountNode) 19 | // }, delay) 20 | 21 | /** 22 | * 动画结束时的回调 23 | */ 24 | const closeAfter = () => { 25 | // message 销毁 26 | render(null, document.body) 27 | } 28 | 29 | // 生成vnode 30 | const vnode = h(Message, { 31 | type, 32 | content, 33 | delay, 34 | closeAfter 35 | }) 36 | // 渲染组件 37 | render(vnode, document.body) 38 | } 39 | -------------------------------------------------------------------------------- /src/libs/message/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | 24 | 97 | 98 | 110 | -------------------------------------------------------------------------------- /src/libs/navbar/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/libs/popover/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 39 | 40 | 150 | 151 | 167 | -------------------------------------------------------------------------------- /src/libs/popup/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 58 | 59 | 93 | -------------------------------------------------------------------------------- /src/libs/search/index.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 69 | 70 | 151 | 152 | 165 | -------------------------------------------------------------------------------- /src/libs/svg-icon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | -------------------------------------------------------------------------------- /src/libs/transition-router-view/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 28 | 29 | 117 | 118 | 181 | -------------------------------------------------------------------------------- /src/libs/trigger-menu-item/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/libs/trigger-menu/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/libs/waterfall/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 35 | 282 | 283 | 284 | -------------------------------------------------------------------------------- /src/libs/waterfall/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取所有item中img元素 3 | */ 4 | 5 | export function getImgElements(itemElements) { 6 | const imgElements = [] 7 | itemElements.forEach((el) => { 8 | imgElements.push(...el.getElementsByTagName('img')) 9 | }) 10 | return imgElements 11 | } 12 | 13 | /** 14 | * 获取所有图片路径 15 | */ 16 | 17 | export function getAllImgSrc(imgElements) { 18 | const allImgSrc = [] 19 | imgElements.forEach((item) => { 20 | allImgSrc.push(item.getAttribute('src')) 21 | }) 22 | return allImgSrc 23 | } 24 | 25 | /** 26 | * 图片预加载, 返回promise 27 | */ 28 | 29 | export function allImgComplete(allImgSrc) { 30 | // 存放所有图片加载的promise对象 31 | const promises = [] 32 | // 循环allImgSrc 33 | allImgSrc.forEach((imgSrc, index) => { 34 | promises.push( 35 | new Promise((resolve) => { 36 | const imgObj = new Image() 37 | imgObj.src = imgSrc 38 | imgObj.onload = () => { 39 | resolve({ 40 | imgSrc, 41 | index 42 | }) 43 | } 44 | }) 45 | ) 46 | }) 47 | return Promise.all(promises) 48 | } 49 | 50 | /** 51 | * 获取最小高度 52 | */ 53 | 54 | export function getMinHeight(columnHeightObj) { 55 | const columnHeightValue = Object.values(columnHeightObj) 56 | return Math.min(...columnHeightValue) 57 | } 58 | 59 | /** 60 | * 获取最小高度的column 61 | */ 62 | 63 | export function getMinHeightColumn(columnHeightObj) { 64 | // 获取最小高度 65 | const minHeight = getMinHeight(columnHeightObj) 66 | const columns = Object.keys(columnHeightObj) 67 | const minHeightColumn = columns.find((col) => { 68 | return columnHeightObj[col] === minHeight 69 | }) 70 | return minHeightColumn 71 | } 72 | 73 | /** 74 | * 获取最大高度 75 | */ 76 | 77 | export function getMaxHeight(columnHeightObj) { 78 | const columnHeightValue = Object.values(columnHeightObj) 79 | return Math.max(...columnHeightValue) 80 | } 81 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import './styles/index.scss' 3 | import App from './App.vue' 4 | import router from './router' 5 | import { useREM } from './utils/flexible' 6 | import libs from './libs' 7 | import directives from './directives' 8 | import store from './store' 9 | // 注册 svg-icons 10 | import 'virtual:svg-icons-register' 11 | import useTheme from '@/utils/theme' 12 | import permission from './permission' 13 | 14 | useREM() 15 | // 注意:onresize事件检测的是布局视口的变化。 16 | window.onresize = useREM 17 | 18 | useTheme() 19 | permission() 20 | 21 | createApp(App).use(router).use(store).use(libs).use(directives).mount('#app') 22 | -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import { createMessage } from './libs' 2 | import router from './router' 3 | import store from './store' 4 | export default function permission() { 5 | router.beforeEach((to, from) => { 6 | // 需要登录,才能访问 7 | if (to.meta.user) { 8 | if (store.getters.token) { 9 | return true 10 | } else { 11 | // 未登录,警告然后返回首页 12 | createMessage({ 13 | type: 'error', 14 | content: '未登录,请登录后访问!' 15 | }) 16 | return '/' 17 | } 18 | } else { 19 | // 不需要登录即可访问 20 | return 21 | } 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import {createRouter, createWebHistory} from "vue-router" 2 | import { isMobileTerminal } from "@/utils/flexible" 3 | import mobileRoutes from "./modules/mobile-routes" 4 | import pcRoutes from "./modules/pc-routes" 5 | 6 | const router = createRouter({ 7 | history: createWebHistory(), 8 | // 根据窗口尺寸,区分移动,pc路由表 9 | routes: isMobileTerminal.value ? mobileRoutes : pcRoutes 10 | }) 11 | 12 | export default router -------------------------------------------------------------------------------- /src/router/modules/mobile-routes.js: -------------------------------------------------------------------------------- 1 | const routes = [ 2 | { 3 | path: '/', 4 | name: 'home', 5 | component: () => import('@/views/main/index.vue') 6 | }, 7 | // { 8 | // path: '/pins/:id', 9 | // name: 'pins', 10 | // component: () => import('@/views/pins/index.vue') 11 | // }, 12 | { 13 | path: '/login', 14 | name: 'login', 15 | component: () => import('@/views/login/index.vue') 16 | }, 17 | { 18 | path: '/register', 19 | name: 'register', 20 | component: () => import('@/views/register/index.vue') 21 | }, 22 | { 23 | path: '/profile', 24 | name: 'profile', 25 | component: () => import('@/views/profile/index.vue'), 26 | meta: { 27 | user: true 28 | } 29 | }, 30 | { 31 | path: '/member', 32 | name: 'member', 33 | component: () => import('@/views/member/index.vue'), 34 | meta: { 35 | user: true 36 | } 37 | } 38 | // { 39 | // path: '/pay/result', 40 | // name: 'payResult', 41 | // component: () => import('@/views/pay/index.vue'), 42 | // meta: { 43 | // user: true 44 | // } 45 | // }, 46 | // { 47 | // path: '/404', 48 | // name: '404', 49 | // component: () => import('@/views/error/404/index.vue') 50 | // }, 51 | // // 404 页面处理 52 | // { 53 | // path: '/:catchAll(.*)', 54 | // name: 'error', 55 | // redirect: '/404' 56 | // } 57 | ] 58 | 59 | export default routes 60 | -------------------------------------------------------------------------------- /src/router/modules/pc-routes.js: -------------------------------------------------------------------------------- 1 | const routes = [ 2 | { 3 | path: '/', 4 | name: 'main', 5 | component: () => import('@/views/layout/index.vue'), 6 | children: [ 7 | { 8 | path: '', 9 | name: 'home', 10 | component: () => import('@/views/main/index.vue') 11 | }, 12 | // { 13 | // path: '/pins/:id', 14 | // name: 'pins', 15 | // component: () => import('@/views/pins/index.vue') 16 | // }, 17 | { 18 | path: '/profile', 19 | name: 'profile', 20 | component: () => import('@/views/profile/index.vue'), 21 | // 标记只有用户登录了才可以进入的页面 22 | meta: { 23 | user: true 24 | } 25 | }, 26 | { 27 | path: '/member', 28 | name: 'member', 29 | component: () => import('@/views/member/index.vue'), 30 | meta: { 31 | user: true 32 | } 33 | } 34 | // { 35 | // path: '/pay/result', 36 | // name: 'payResult', 37 | // component: () => import('@/views/pay/index.vue'), 38 | // meta: { 39 | // user: true 40 | // } 41 | // } 42 | ] 43 | }, 44 | { 45 | path: '/login', 46 | name: 'login', 47 | component: () => import('@/views/login/index.vue') 48 | }, 49 | { 50 | path: '/register', 51 | name: 'register', 52 | component: () => import('@/views/register/index.vue') 53 | } 54 | // { 55 | // path: '/404', 56 | // name: '404', 57 | // component: () => import('@/views/error/404/index.vue') 58 | // }, 59 | // // 404 页面处理 60 | // { 61 | // path: '/:catchAll(.*)', 62 | // name: 'error', 63 | // redirect: '/404' 64 | // } 65 | ] 66 | 67 | export default routes 68 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 便捷访问每个模块下的数据 3 | */ 4 | import { isMobileTerminal } from '@/utils/flexible' 5 | 6 | export default { 7 | categorys: (state) => state.category.categorys, 8 | themeType: (state) => state.theme.themeType, 9 | // 当前分类 10 | currentCategory: (state) => state.app.currentCategory, 11 | // 当前分类下标 12 | currentCategoryIndex: (state, getters) => { 13 | return getters.categorys.findIndex((item) => { 14 | return item.id === getters.currentCategory.id 15 | }) 16 | }, 17 | // 历史数据 18 | historyWords: (state) => state.search.historyWords, 19 | // 搜索文本 20 | searchValue: (state) => state.app.searchValue, 21 | token(state) { 22 | return state.user.token 23 | }, 24 | // 用户信息 25 | userInfo(state) { 26 | return state.user.userInfo 27 | }, 28 | // 路由跳转类型 29 | routerType(state) { 30 | // 移动端 31 | if (isMobileTerminal.value) { 32 | return state.app.routerType 33 | } 34 | // pc端直接返回none 35 | return 'none' 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | import getters from './getters' 3 | import category from './modules/category' 4 | import theme from './modules/theme' 5 | import app from './modules/app' 6 | import search from './modules/search' 7 | import user from './modules/user' 8 | import createPersistedState from 'vuex-persistedstate' 9 | const store = createStore({ 10 | state: {}, 11 | getters, 12 | modules: { 13 | category, 14 | theme, 15 | app, 16 | search, 17 | user 18 | }, 19 | // 缓存数据 20 | plugins: [ 21 | createPersistedState({ 22 | // 指定保存到localStorage中的key值 23 | key: 'categoryList', 24 | // 需要保存的模块 25 | paths: ['category', 'theme', 'search', 'user'] 26 | }) 27 | ] 28 | }) 29 | 30 | export default store 31 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 用于存放联动数据。 不需要缓存 3 | */ 4 | 5 | import { ALL_CATEGORY } from '@/constants' 6 | 7 | export default { 8 | namespaced: true, 9 | state: () => { 10 | return { 11 | // 初始值为全部 12 | currentCategory: ALL_CATEGORY, 13 | // 搜索文本 14 | searchValue: '', 15 | // 路由跳转类型 16 | routerType: 'none' 17 | } 18 | }, 19 | mutations: { 20 | setCurrentCategory(state, category) { 21 | state.currentCategory = category 22 | }, 23 | setSearchValue(state, value) { 24 | state.searchValue = value 25 | }, 26 | setRouterType(state, value) { 27 | state.routerType = value 28 | } 29 | }, 30 | actions: {} 31 | } 32 | -------------------------------------------------------------------------------- /src/store/modules/category.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 处理navigationBar中的数据 categorys 3 | */ 4 | 5 | import { getCategoryList } from '@/api/category' 6 | import { CATEGORY_NOMAR_DATA, ALL_CATEGORY } from '@/constants' 7 | export default { 8 | namespaced: true, 9 | state: () => { 10 | return { 11 | categorys: CATEGORY_NOMAR_DATA 12 | } 13 | }, 14 | mutations: { 15 | // 获取categorys数据 16 | setCategorys(state, categorys) { 17 | state.categorys = [ALL_CATEGORY, ...categorys] 18 | } 19 | }, 20 | actions: { 21 | // 获取categorys数据 22 | async getCategoryList({ commit }, payload) { 23 | const res = await getCategoryList() 24 | commit('setCategorys', res.categorys) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/store/modules/search.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 搜索相关数据 3 | */ 4 | 5 | export default { 6 | namespaced: true, 7 | state: () => { 8 | return { 9 | // 历史搜索记录 10 | historyWords: [] 11 | } 12 | }, 13 | mutations: { 14 | /** 15 | * 添加: 相同的删除后面的,将该值加入最前面 16 | */ 17 | addHistoryWords(state, word) { 18 | const index = state.historyWords.findIndex((item) => item === word) 19 | if (index !== -1) { 20 | state.historyWords.splice(index, 1) 21 | } 22 | state.historyWords.unshift(word) 23 | }, 24 | 25 | /** 26 | * 删除单个 27 | */ 28 | deleteHistoryWords(state, index) { 29 | state.historyWords.splice(index, 1) 30 | }, 31 | 32 | /** 33 | * 删除全部 34 | */ 35 | removeHistoryWords(state) { 36 | state.historyWords = [] 37 | } 38 | }, 39 | actions: {} 40 | } 41 | -------------------------------------------------------------------------------- /src/store/modules/theme.js: -------------------------------------------------------------------------------- 1 | // 当前主题模式 2 | import { THEME_LIGHT } from '@/constants' 3 | export default { 4 | namespaced: true, 5 | state: () => ({ 6 | themeType: THEME_LIGHT 7 | }), 8 | mutations: { 9 | setThemeType(state, theme) { 10 | state.themeType = theme 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 用户相关数据 3 | */ 4 | import { postLogin, getProfile } from '@/api/sys' 5 | import md5 from 'md5' 6 | import { createMessage } from '../../libs' 7 | 8 | export default { 9 | namespaced: true, 10 | state: () => { 11 | return { 12 | token: '', 13 | userInfo: {} 14 | } 15 | }, 16 | mutations: { 17 | setToken(state, token) { 18 | state.token = token 19 | }, 20 | setUserInfo(state, userinfo) { 21 | state.userInfo = userinfo 22 | }, 23 | // 退出登录 24 | postLogout(state) { 25 | // 清空一些数据 26 | state.token = '' 27 | state.userInfo = {} 28 | // 刷新页面 29 | location.reload() 30 | } 31 | }, 32 | actions: { 33 | async changToken({ commit, dispatch }, payload) { 34 | // 加密密码 35 | const password = md5(payload.password) 36 | const { token } = await postLogin({ 37 | ...payload, 38 | password: password ? password : '' 39 | }) 40 | commit('setToken', token) 41 | 42 | // 获取用户信息 43 | dispatch('changeUserInfo') 44 | }, 45 | async changeUserInfo({ commit }) { 46 | const res = await getProfile() 47 | commit('setUserInfo', res) 48 | createMessage({ 49 | type: 'success', 50 | content: `欢迎${ 51 | res.vipLevel ? `vip用户${res.username}` : `用户${res.username}` 52 | }登录` 53 | }) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | a { 27 | font-weight: 500; 28 | color: #646cff; 29 | text-decoration: inherit; 30 | } 31 | a:hover { 32 | color: #535bf2; 33 | } 34 | 35 | body { 36 | margin: 0; 37 | display: flex; 38 | place-items: center; 39 | min-width: 320px; 40 | min-height: 100vh; 41 | } 42 | 43 | h1 { 44 | font-size: 3.2em; 45 | line-height: 1.1; 46 | } 47 | 48 | button { 49 | border-radius: 8px; 50 | border: 1px solid transparent; 51 | padding: 0.6em 1.2em; 52 | font-size: 1em; 53 | font-weight: 500; 54 | font-family: inherit; 55 | background-color: #1a1a1a; 56 | cursor: pointer; 57 | transition: border-color 0.25s; 58 | } 59 | button:hover { 60 | border-color: #646cff; 61 | } 62 | button:focus, 63 | button:focus-visible { 64 | outline: 4px auto -webkit-focus-ring-color; 65 | } 66 | 67 | .card { 68 | padding: 2em; 69 | } 70 | 71 | #app { 72 | max-width: 1280px; 73 | margin: 0 auto; 74 | padding: 2rem; 75 | text-align: center; 76 | } 77 | 78 | @media (prefers-color-scheme: light) { 79 | :root { 80 | color: #213547; 81 | background-color: #ffffff; 82 | } 83 | a:hover { 84 | color: #747bff; 85 | } 86 | button { 87 | background-color: #f9f9f9; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | // 解决引导组件的背景覆盖问题 7 | .driver-fix-stacking { 8 | position: relative; 9 | } 10 | 11 | // 解决引导组件按钮未对齐问题 12 | .driver-navigation-btns { 13 | line-height: 0; 14 | } -------------------------------------------------------------------------------- /src/utils/color.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 生成随机色值 3 | */ 4 | export default function randomRGB() { 5 | const r = Math.floor(Math.random() * 256) 6 | const g = Math.floor(Math.random() * 256) 7 | const b = Math.floor(Math.random() * 256) 8 | return `rgb(${r}, ${g}, ${b})` 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/flexible.js: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { PC_DEVICE_WIDTH } from '@/constants' 3 | import { useWindowSize } from '@vueuse/core' 4 | 5 | const { width } = useWindowSize() 6 | /** 7 | * 是否是移动端设备; 判断依据: 屏幕宽度小于 PC_DEVICE_WIDTH 8 | * @returns 9 | */ 10 | export const isMobileTerminal = computed(() => { 11 | // 数据非响应,只会执行一次。computed缓存了 12 | // return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( 13 | // navigator.userAgent 14 | // ) 15 | return width.value < PC_DEVICE_WIDTH 16 | }) 17 | 18 | /** 19 | * 改变根字体大小,用于界面适配 20 | */ 21 | 22 | export function useREM() { 23 | //获取布局视口宽度,因为开启了理想视口,布局视口=设备横向独立像素值 24 | const dpWidth = document.documentElement.clientWidth 25 | //计算根字体大小 26 | const rootFontSize = Math.min(40, dpWidth / 10) 27 | //设置根字体大小 28 | document.documentElement.style.fontSize = rootFontSize + 'px' 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import md5 from 'md5' 2 | import { ref } from 'vue' 3 | 4 | export const getSecret = () => { 5 | const codetype = Number.parseInt(Date.now() / Math.pow(10, 3)) 6 | const icode = md5(`${codetype}LGD_Sunday-1991-12-30`) 7 | return { codetype, icode } 8 | } 9 | 10 | export const scrollTransition = () => { 11 | let timer = null 12 | return function exec({ 13 | el = document.body, 14 | position = 0, 15 | direction = 'v', 16 | time = 150 17 | } = options) { 18 | clearInterval(timer) 19 | // 每步的时间 ms 20 | const TIME_EVERY_STEP = 5 21 | // 最大滚动距离 22 | const maxScrollSize = el.scrollWidth - el.offsetWidth 23 | // 限定position的有效滚动范围 24 | position = Math.max(Math.min(position, maxScrollSize), 0) 25 | // 可以分为多少步 26 | let steps = Math.ceil(time / TIME_EVERY_STEP) 27 | const stepSize = (position - el.scrollLeft) / steps // 每步的长度 28 | 29 | timer = setInterval(() => { 30 | // console.log(el.scrollLeft , position) 31 | if (el.scrollLeft !== Number.parseInt(position) && position >= 0) { 32 | if (stepSize >= 0) { 33 | let scrollX = 34 | el.scrollLeft + stepSize >= position 35 | ? position 36 | : el.scrollLeft + stepSize 37 | el.scrollLeft = scrollX 38 | } else { 39 | let scrollX = 40 | el.scrollLeft + stepSize <= position 41 | ? position 42 | : el.scrollLeft + stepSize 43 | el.scrollLeft = scrollX 44 | } 45 | } else { 46 | clearInterval(timer) 47 | } 48 | }, TIME_EVERY_STEP) 49 | } 50 | } 51 | 52 | /** 53 | * 创建随机颜色 54 | * @returns 55 | */ 56 | export const createRandomColor = () => { 57 | const randomNum = () => Math.floor(Math.random() * 256) 58 | return `rgba(${randomNum()}, ${randomNum()}, ${randomNum()})` 59 | } 60 | 61 | /** 62 | * 防抖函数 63 | * @param {*} cb 64 | * @param {*} time 65 | * @returns 66 | */ 67 | export const debounce = (cb, time) => { 68 | let timer = null 69 | return (...aug) => { 70 | clearTimeout(timer) 71 | timer = setTimeout(() => { 72 | cb && cb.apply(this, aug) 73 | }, time) 74 | } 75 | } 76 | 77 | /** 78 | * 可控定时器 79 | * @param {*} time 80 | * @param {*} cb 81 | * @returns 82 | */ 83 | export const contralTimeout = (time, cb) => { 84 | // 是否正在启动 85 | const isStart = ref(false) 86 | const isFinish = ref(false) 87 | let relTime = 0 88 | let timer = setInterval(() => { 89 | if (isStart.value) { 90 | relTime += 5 91 | } 92 | if (relTime >= time) { 93 | clearInterval(timer) 94 | isFinish.value = true 95 | cb && cb() 96 | } 97 | }, 5) 98 | const stop = () => { 99 | isStart.value = false 100 | } 101 | const start = () => { 102 | isStart.value = true 103 | } 104 | 105 | return { 106 | stop, 107 | start, 108 | isStart, 109 | isFinish 110 | } 111 | } 112 | 113 | 114 | export const useMobileScroll = (el) => { 115 | var overscroll = function(el) { 116 | el.addEventListener('touchstart', function() { 117 | var top = el.scrollTop 118 | , totalScroll = el.scrollHeight 119 | , currentScroll = top + el.offsetHeight; 120 | if(top === 0) { 121 | el.scrollTop = 1; 122 | } else if(currentScroll === totalScroll) { 123 | el.scrollTop = top - 1; 124 | } 125 | }); 126 | el.addEventListener('touchmove', function(evt) { 127 | if(el.offsetHeight < el.scrollHeight) 128 | evt._isScroller = true; 129 | }); 130 | } 131 | overscroll(el); 132 | } -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { getSecret } from './index' 3 | import store from '../store' 4 | import { createMessage } from '@/libs' 5 | 6 | const service = axios.create({ 7 | baseURL: import.meta.env.VITE_BASE_API, 8 | timeout: 5000 9 | }) 10 | 11 | service.interceptors.response.use( 12 | (response) => { 13 | const { success, data, message } = response.data 14 | // 要根据success的成功与否决定下面的操作 15 | if (success) { 16 | return data 17 | } else { 18 | createMessage({ 19 | type: 'error', 20 | content: message 21 | }) 22 | // TODO:业务错误 23 | return Promise.reject(new Error(message)) 24 | } 25 | }, 26 | // 状态码不是200 27 | (error) => { 28 | // 处理 token 超时问题 29 | if ( 30 | error.response && 31 | error.response.data && 32 | error.response.data.code === 401 33 | ) { 34 | // TODO: token超时 35 | store.commit('user/postLogout') 36 | } 37 | createMessage({ 38 | type: 'error', 39 | content: error.response.data.message 40 | }) 41 | // TODO: 提示错误消息 42 | return Promise.reject(error) 43 | } 44 | ) 45 | 46 | service.interceptors.request.use((config) => { 47 | config.headers = { 48 | ...getSecret() 49 | } 50 | // user 51 | if (config.url.includes('user') && store.getters.token) { 52 | // 如果token存在 注入token 53 | config.headers.Authorization = `Bearer ${store.getters.token}` 54 | } 55 | // TODO 错误处理 56 | return config 57 | }) 58 | 59 | export default service 60 | -------------------------------------------------------------------------------- /src/utils/sts.js: -------------------------------------------------------------------------------- 1 | import OSS from 'ali-oss' 2 | import { REGION, BUCKET } from '@/constants' 3 | import { getSts } from '@/api/sys' 4 | 5 | export const getOSSClient = async () => { 6 | const res = await getSts() 7 | return new OSS({ 8 | // yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。 9 | region: REGION, 10 | // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。 11 | accessKeyId: res.Credentials.AccessKeyId, 12 | accessKeySecret: res.Credentials.AccessKeySecret, 13 | // 从STS服务获取的安全令牌(SecurityToken)。 14 | stsToken: res.Credentials.SecurityToken, 15 | // 填写Bucket名称。 16 | bucket: BUCKET, 17 | // 刷新 token,在 token 过期后自动调用(但是并不生效,可能会在后续的版本中修复) 18 | refreshSTSToken: async () => { 19 | // 向您搭建的STS服务获取临时访问凭证。 20 | const res = await getSts() 21 | return { 22 | accessKeyId: res.Credentials.AccessKeyId, 23 | accessKeySecret: res.Credentials.AccessKeySecret, 24 | stsToken: res.Credentials.SecurityToken 25 | } 26 | }, 27 | // 刷新临时访问凭证的时间间隔,单位为毫秒。 28 | refreshSTSTokenInterval: 5 * 1000 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/theme.js: -------------------------------------------------------------------------------- 1 | import { watch } from 'vue' 2 | import store from '../store' 3 | import { THEME_DARK, THEME_LIGHT, THEME_SYSTEM } from '../constants' 4 | 5 | /** 6 | * 监听系统主题变化 7 | */ 8 | let matchMedia = '' 9 | function changeSystemTheme() { 10 | // 仅需初始化一次即可 11 | if (matchMedia) return 12 | matchMedia = window.matchMedia('(prefers-color-scheme: dark)') 13 | 14 | // let systemTheme = '' 15 | // matchMedia.addEventListener('change', (event) => { 16 | // // true是深色模式, false是浅色主题 17 | // // systemTheme = event.matches ? THEME_DARK : THEME_LIGHT 18 | // }) 19 | // return systemTheme 20 | 21 | matchMedia.addEventListener('change', (event) => { 22 | // true是深色模式, false是浅色主题 23 | changeTheme(THEME_SYSTEM) 24 | }) 25 | } 26 | 27 | /** 28 | * 主题匹配函数 29 | * @param val {*} 主题标记 30 | */ 31 | const changeTheme = (val) => { 32 | let htmlClass = '' 33 | if (val === THEME_LIGHT) { 34 | // 浅色主题 35 | htmlClass = THEME_LIGHT 36 | } else if (val === THEME_DARK) { 37 | // 深色主题 38 | htmlClass = THEME_DARK 39 | } else { 40 | // 跟随系统 41 | changeSystemTheme() 42 | htmlClass = matchMedia.matches ? THEME_DARK : THEME_LIGHT 43 | } 44 | document.querySelector('html').className = htmlClass 45 | } 46 | 47 | /** 48 | * 初始化主题 49 | */ 50 | export default () => { 51 | // 监听主题切换,修改html class的值 52 | watch(() => store.getters.themeType, changeTheme, { 53 | immediate: true 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /src/vendor/SliderCaptcha/slidercaptcha.min.css: -------------------------------------------------------------------------------- 1 | .slider-captcha-block { 2 | position: absolute; 3 | left: 0; 4 | top: 0; 5 | } 6 | .card { 7 | display: flex; 8 | flex-direction: column; 9 | min-width: 0; 10 | word-wrap: break-word; 11 | background-clip: border-box; 12 | border: 1px solid rgba(0, 0, 0, 0.125); 13 | } 14 | .card-header { 15 | padding: 0.75rem 1.25rem; 16 | margin-bottom: 0; 17 | background-color: rgba(0, 0, 0, 0.03); 18 | border-bottom: 1px solid rgba(0, 0, 0, 0.125); 19 | } 20 | .card-header:first-child { 21 | border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; 22 | } 23 | .card-body { 24 | flex: 1 1 auto; 25 | padding: 1.25rem; 26 | } 27 | .sliderContainer { 28 | position: relative; 29 | text-align: center; 30 | line-height: 40px; 31 | background: #f7f9fa; 32 | color: #45494c; 33 | border-radius: 2px; 34 | } 35 | .sliderbg { 36 | position: absolute; 37 | left: 0; 38 | right: 0; 39 | top: 0; 40 | background-color: #f7f9fa; 41 | height: 40px; 42 | border-radius: 2px; 43 | border: 1px solid #e6e8eb; 44 | } 45 | .sliderContainer_active .slider { 46 | top: -1px; 47 | border: 1px solid #1991fa; 48 | } 49 | .sliderContainer_active .sliderMask { 50 | border-width: 1px 0 1px 1px; 51 | } 52 | .sliderContainer_success .slider { 53 | top: -1px; 54 | border: 1px solid #52ccba; 55 | background-color: #52ccba !important; 56 | } 57 | .sliderContainer_success .sliderMask { 58 | border: 1px solid #52ccba; 59 | border-width: 1px 0 1px 1px; 60 | background-color: #d2f4ef; 61 | } 62 | .sliderContainer_success .sliderIcon:before { 63 | content: '\e62f'; 64 | } 65 | .sliderContainer_fail .slider { 66 | top: -1px; 67 | border: 1px solid #f57a7a; 68 | background-color: #f57a7a !important; 69 | } 70 | .sliderContainer_fail .sliderMask { 71 | border: 1px solid #f57a7a; 72 | background-color: #fce1e1; 73 | border-width: 1px 0 1px 1px; 74 | } 75 | .sliderContainer_fail .sliderIcon:before { 76 | content: '\e61d'; 77 | } 78 | .sliderContainer_active .sliderText, 79 | .sliderContainer_success .sliderText, 80 | .sliderContainer_fail .sliderText { 81 | display: none; 82 | } 83 | .sliderMask { 84 | position: absolute; 85 | left: 0; 86 | top: 0; 87 | height: 40px; 88 | border: 0 solid #1991fa; 89 | background: #d1e9fe; 90 | border-radius: 2px; 91 | } 92 | .slider { 93 | position: absolute; 94 | top: 0; 95 | left: 0; 96 | width: 40px; 97 | height: 40px; 98 | background: #fff; 99 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.3); 100 | cursor: pointer; 101 | transition: background 0.2s linear; 102 | border-radius: 2px; 103 | display: flex; 104 | align-items: center; 105 | justify-content: center; 106 | } 107 | .slider:hover { 108 | background: #1991fa; 109 | } 110 | .slider:hover .sliderIcon { 111 | background-position: 0 -13px; 112 | } 113 | .sliderText { 114 | position: relative; 115 | } 116 | .refreshIcon { 117 | position: absolute; 118 | right: 0; 119 | top: 0; 120 | cursor: pointer; 121 | margin: 6px; 122 | color: rgba(0, 0, 0, 0.25); 123 | font-size: 1rem; 124 | z-index: 5; 125 | transition: color 0.3s linear; 126 | } 127 | .refreshIcon:hover { 128 | color: #6c757d; 129 | } 130 | 131 | .slidercaptcha { 132 | margin: 0 auto; 133 | width: 314px; 134 | height: 286px; 135 | border-radius: 4px; 136 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.125); 137 | margin-top: 40px; 138 | } 139 | 140 | .slidercaptcha .card-body { 141 | padding: 1rem; 142 | } 143 | 144 | .slidercaptcha canvas:first-child { 145 | border-radius: 4px; 146 | border: 1px solid #e6e8eb; 147 | } 148 | 149 | .slidercaptcha.card .card-header { 150 | background-image: none; 151 | background-color: rgba(0, 0, 0, 0.03); 152 | } 153 | 154 | .refreshIcon { 155 | top: -54px; 156 | } 157 | -------------------------------------------------------------------------------- /src/views/layout/components/floating/index.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 76 | 77 | 83 | -------------------------------------------------------------------------------- /src/views/layout/components/floating/steps.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | // 在哪个元素中高亮 4 | element: '.guide-home', 5 | // 配置对象 6 | popover: { 7 | // 标题 8 | title: 'logo', 9 | // 描述 10 | description: '点击可返回首页' 11 | } 12 | }, 13 | { 14 | element: '.guide-search', 15 | popover: { 16 | title: '搜索', 17 | description: '搜索您期望的图片' 18 | } 19 | }, 20 | { 21 | element: '.guide-theme', 22 | popover: { 23 | title: '风格', 24 | description: '选择一个您喜欢的风格', 25 | // 弹出的位置 26 | position: 'left' 27 | } 28 | }, 29 | { 30 | element: '.guide-my', 31 | popover: { 32 | title: '账户', 33 | description: '这里标记了您的账户信息', 34 | position: 'left' 35 | } 36 | }, 37 | { 38 | element: '.guide-start', 39 | popover: { 40 | title: '引导', 41 | description: '这里可再次查看引导信息', 42 | position: 'left' 43 | } 44 | }, 45 | { 46 | element: '.guide-feedback', 47 | popover: { 48 | title: '反馈', 49 | description: '您的任何不满都可以在这里告诉我们', 50 | position: 'left' 51 | } 52 | } 53 | ] 54 | -------------------------------------------------------------------------------- /src/views/layout/components/header/header-my.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/views/layout/components/header/header-search/hint.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/views/layout/components/header/header-search/history.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/views/layout/components/header/header-search/hot.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/views/layout/components/header/header-search/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/views/layout/components/header/header-theme.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/views/layout/components/header/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/views/layout/components/main/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/views/layout/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/views/login/components/header.vue: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /src/views/login/components/qq-login.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/views/login/components/slider-captcha.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/views/login/components/wechat-login.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /src/views/login/validate.js: -------------------------------------------------------------------------------- 1 | // 验证函数,true表示表单验证通过, String表示表单验证未通过,给出的提示文本。 2 | 3 | /** 4 | * 用户名的表单校验 5 | */ 6 | export const validateUsername = (value) => { 7 | if (!value) { 8 | return '用户名为必填的' 9 | } 10 | 11 | if (value.length < 3 || value.length > 12) { 12 | return '用户名应该在 3-12 位之间' 13 | } 14 | return true 15 | } 16 | 17 | /** 18 | * 密码的表单校验 19 | */ 20 | export const validatePassword = (value) => { 21 | if (!value) { 22 | return '密码为必填的' 23 | } 24 | 25 | if (value.length < 6 || value.length > 12) { 26 | return '密码应该在 6-12 位之间' 27 | } 28 | return true 29 | } 30 | 31 | /** 32 | * 确认密码的表单校验 33 | * 34 | * 参数二:表示关联表单值的数组 35 | */ 36 | export const validateConfirmPassword = (value, password) => { 37 | if (value !== password[0]) { 38 | return '两次密码输入必须一致' 39 | } 40 | return true 41 | } 42 | -------------------------------------------------------------------------------- /src/views/main/components/list/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/views/main/components/list/item.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/views/main/components/menu/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/views/main/components/navigation/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /src/views/main/components/navigation/mobile/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/views/main/components/navigation/pc/index.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/views/main/index.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 50 | 51 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/views/member/components/pay-menu-item.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/views/member/components/payment/discounts.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 27 | -------------------------------------------------------------------------------- /src/views/member/components/payment/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /src/views/member/components/payment/mobile-payment/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 49 | -------------------------------------------------------------------------------- /src/views/member/components/payment/mobile-payment/mobile-pay-select.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 31 | -------------------------------------------------------------------------------- /src/views/member/components/payment/pc-payment/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/views/member/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/views/profile/components/change-avatar.vue: -------------------------------------------------------------------------------- 1 | 22 | 43 | 44 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /src/views/register/index.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | // 手动切换暗模式,可以通过data-mode自定义css前缀名字。默认是dark 4 | // darkMode: ['class', '[data-mode="dark"]'], 5 | darkMode: 'class', 6 | // tailwindcss 应用的文件范围 7 | content: ['./index.html', './src/**/*.{vue,js}'], 8 | theme: { 9 | extend: { 10 | // [rem预设值, lineHeight预设值] 11 | fontSize: { 12 | xs: ['0.25rem', '0.35rem'], 13 | sm: ['0.35rem', '0.45rem'], 14 | base: ['0.42rem', '0.52rem'], 15 | lg: ['0.55rem', '0.65rem'], 16 | xl: ['0.65rem', '0.75rem'] 17 | }, 18 | height: { 19 | header: '72px', 20 | main: 'calc(100vh - 72px)' 21 | }, 22 | boxShadow: { 23 | 'l-white': '-10px 0 10px white', 24 | 'l-zinc': '-10px 0 10px #18181b' 25 | }, 26 | colors: { 27 | main: '#f44c58', 28 | 'hover-main': '#f32836', 29 | 'success-100': '#F2F9EC', 30 | 'success-200': '#E4F2DB', 31 | 'success-300': '#7EC050', 32 | 'warn-100': '#FCF6ED', 33 | 'warn-200': '#F8ECDA', 34 | 'warn-300': '#DCA550', 35 | 'error-100': '#ED7456', 36 | 'error-200': '#f3471c', 37 | 'error-300': '#ffffff' 38 | }, 39 | backdropBlur: { 40 | '4xl': '240px' 41 | }, 42 | variants: { 43 | scrollbar: ['dark'] 44 | } 45 | } 46 | }, 47 | plugins: [] 48 | } 49 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import {join} from "path" 4 | import {createSvgIconsPlugin} from "vite-plugin-svg-icons" 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | vue(), 10 | createSvgIconsPlugin({ 11 | // 指定需要缓存的图标文件夹 12 | iconDirs: [join(__dirname, "/src/assets/icons")], 13 | // 指定symbolId格式,就是svg.use使用的href 14 | symbolId: "icon-[name]" 15 | }) 16 | ], 17 | resolve: { 18 | alias: { 19 | "@": join(__dirname, "/src") 20 | } 21 | }, 22 | server: { 23 | proxy: { 24 | "/api": { 25 | target: "https://api.imooc-front.lgdsunday.club/", 26 | // 改变请求的origin为target的值 27 | changeOrigin: true, 28 | // rewrite: (path) => path.replace(/^\/api/, ''), 29 | } 30 | // '/prod-api': { 31 | // target: 'https://imooc-front.lgdsunday.club/', 32 | // changeOrigin: true 33 | // } 34 | } 35 | } 36 | }) 37 | --------------------------------------------------------------------------------