diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d4e27b --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# 依赖相关 (npm/yarn) +node_modules/ +package-lock.json +yarn.lock + +# 编译/构建产物 +dist/ + +# Docker 相关 +.docker-compose/ + +# 编辑器/IDE 配置 +.idea/ +.vscode/ +*.suo +*.ntvs* +*.njsproj +*.sln + +# 脚本/可执行文件 +*.sh +fpx.bat + +# 压缩包 +*.zip +*.rar +*.7z +*.tar + +# 环境配置文件 (核心:忽略所有环境文件) +.env +.env.development +.env.production +.env.line +.env.outnet +.env.test + +# 其他临时文件 +*.env \ No newline at end of file diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..e955840 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/cli-plugin-babel/preset' + ] +} diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..4f47c85 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,12 @@ +/// + +// 声明你的环境变量,按需添加 +interface ImportMetaEnv { + readonly VITE_BASE_PATH: string + // 继续添加你项目里其他 VITE_ 开头变量 + // readonly VITE_ROUTER_MODE: string + } + + interface ImportMeta { + readonly env: ImportMetaEnv + } \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..fe4530c Binary files /dev/null and b/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..64c1c22 --- /dev/null +++ b/index.html @@ -0,0 +1,23 @@ + + + + + + + + + %VITE_TITLE% + + + + + +
+ + + + + \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..4aafc5f --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "esnext", + "baseUrl": "./", + "moduleResolution": "node", + "paths": { + "@/*": [ + "src/*" + ] + }, + "lib": [ + "esnext", + "dom", + "dom.iterable", + "scripthost" + ] + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8864589 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "work-show-web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "autoprefixer": "^10.5.0", + "axios": "^1.16.1", + "core-js": "^3.8.3", + "element-plus": "^2.14.1", + "postcss": "^8.5.15", + "vue": "^3.2.13", + "vue-router": "^4.0.3", + "vue-waterfall-plugin-next": "^3.0.1", + "vue3-waterfall-plugin": "^1.1.5", + "vuex": "^4.0.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.7", + "@vue/cli-plugin-typescript": "^5.0.9", + "sass": "^1.32.7", + "sass-loader": "^12.0.0", + "tailwindcss": "3", + "typescript": "^6.0.3", + "vite": "^8.0.15" + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not dead", + "not ie 11" + ] +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..7c1c9d3 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: [ + require('tailwindcss'), + require('autoprefixer') + ] + } \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/public/favicon.ico differ diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..4429eaf --- /dev/null +++ b/src/App.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..1efefb9 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,7 @@ +import request from "./request"; +import works from "./works"; +interface ApiInterface { + key: Function; +} +const apis: ApiInterface = Object.assign({}, works(request)); +export default apis; diff --git a/src/api/request.ts b/src/api/request.ts new file mode 100644 index 0000000..4807d3f --- /dev/null +++ b/src/api/request.ts @@ -0,0 +1,153 @@ +import axios, { + AxiosInstance, + AxiosRequestConfig, + AxiosResponse, + AxiosError, +} from "axios"; +import { showMsg } from "@/utils/showMsg"; +import {showLoading,hiddenLoading} from "@/utils/loading"; +// 定义响应数据结构 +interface ResponseData { + code: number; + data: T; + message?: string; +} + +// 定义配置扩展接口 +interface CustomAxiosRequestConfig extends AxiosRequestConfig { + donNotShowLoading?: boolean; +} + +// 创建axios实例 +const service: AxiosInstance = axios.create({ + baseURL: import.meta.env.VITE_BASE_PATH, + timeout: 1000 * 10 * 60, // 原有超时配置(600秒) +}); + +// ========== 新增:辅助函数 - 判断是否为无网络状态 ========== +const isOffline = () => { + // 浏览器原生API,判断当前是否处于离线状态 + return typeof window !== "undefined" && !window.navigator.onLine; +}; + +// 请求拦截器(保留原有业务逻辑,无修改) +service.interceptors.request.use( + (config: any) => { + const { method } = config; + if (method === "get" && config.params) { + config.data = config.params; + } + config.headers = { + "Content-Type": "application/json", + // [import.meta.env.VITE_SESS_KEY]: `Bearer ${userStore.token}`, + ...config.headers, + }; + return config; + }, + (error: AxiosError) => { + const config = error.config as CustomAxiosRequestConfig; + showMsg(error.message || "请求错误", "error"); + return Promise.reject(error); + } +); + +// 响应拦截器(新增:超时 + 无网络判断) +service.interceptors.response.use( + async (response: AxiosResponse) => { + const { data, headers } = response; + + // 处理zip文件响应 + if (headers["content-type"] === "application/zip") { + return data; + } + // 处理Blob响应 + if (data instanceof Blob) { + return data; + } + + let { code, data: obj, message } = data; + + if (code === 200) { + hiddenLoading(); + // showMsg("请求成功", "success"); + if (headers.message) { + message = decodeURI(headers.message); + } + return obj; + } else { + message && showMsg(message || decodeURI(headers.message), "error"); + hiddenLoading(); + return Promise.reject(data); + } + }, + async (error: AxiosError) => { + let { message, response, config: requestConfig } = error; + let config = requestConfig as CustomAxiosRequestConfig; + console.log("接口报错", response, error.code); + + // ========== 新增核心1:超时错误判断(Axios错误码 ECONNABORTED) ========== + const isTimeout = error.code === AxiosError.ECONNABORTED; + + // ========== 新增核心2:无网络判断(结合原生API + 无response) ========== + const noNetwork = isOffline(); + + // 无响应的场景(超时 / 无网络 / 其他网络异常) + if (!response) { + // 区分提示:无网络 > 超时 > 通用网络异常 + if (noNetwork) { + message = "当前无网络连接,请检查网络后重试"; + } else if (isTimeout) { + message = "请求超时,服务器响应过慢,请稍后重试"; + } else { + message = "网络异常,请检查网络设置"; + } + showMsg(message, "error"); + hiddenLoading(); + return Promise.reject(error); + } + + // 有响应但状态码错误的场景(保留原有逻辑) + let { status, data } = response; + if (data) { + data?.message && (message = data.message); + data?.code && (error.code = data.code); + } + switch (status) { + case 401: + // showMsg.confirm({ + // title: "登录过期", + // message: message || "登录过期", + // confirmButtonText: "重新登录", + // cancelButtonText: "取消", + // showCancelButton: false, + // closeOnPopstate: false, + // closeOnClickOverlay: false, + // }); + break; + case 404: + // await showMsg.confirm({ + // title: "请求失败", + // message: message || "请求失败,状态码:404", + // confirmButtonText: "我知道了", + // showCancelButton: false, + // }); + break; + case 500: + showMsg(message || `服务器错误`); + break; + default: + // ========== 新增:默认状态码也补充无网络/超时提示(兜底) ========== + if (noNetwork) { + message = "当前无网络连接,请检查网络后重试"; + } else if (isTimeout) { + message = "请求超时,服务器响应过慢,请稍后重试"; + } + showMsg(message || `请求失败,状态码:${status}`); + } + + hiddenLoading(); + return Promise.reject(error); + } +); + +export default service; \ No newline at end of file diff --git a/src/api/works.js b/src/api/works.js new file mode 100644 index 0000000..340da54 --- /dev/null +++ b/src/api/works.js @@ -0,0 +1,25 @@ +export default ({ request }) => ({ + // 作品集详情 + WOEKS_LIST_ONE(params) { + return request({ + url: `/public/page/product`, + method: "get", + params, + }); + }, + WOEKS_LIST_TWO(params) { + return request({ + url: `/public/page/task/finish`, + method: "get", + params, + }); + }, + /* 根据类型获取字典列表 */ + GET_DICT_LIST(type, params = {}) { + return request({ + url: `/common/dict/select/${type}`, + method: 'get', + params + }) + } +}); diff --git a/src/assets/images/img-load-err.png b/src/assets/images/img-load-err.png new file mode 100644 index 0000000..15044da Binary files /dev/null and b/src/assets/images/img-load-err.png differ diff --git a/src/assets/images/profile.jpg b/src/assets/images/profile.jpg new file mode 100644 index 0000000..9acf6f9 Binary files /dev/null and b/src/assets/images/profile.jpg differ diff --git a/src/assets/images/start.png b/src/assets/images/start.png new file mode 100644 index 0000000..a745d32 Binary files /dev/null and b/src/assets/images/start.png differ diff --git a/src/assets/index.js b/src/assets/index.js new file mode 100644 index 0000000..e3f16e6 --- /dev/null +++ b/src/assets/index.js @@ -0,0 +1 @@ +import "../styles/css/global.scss" \ No newline at end of file diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..fe4530c Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..9eb3f31 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,9 @@ +import { createApp } from 'vue' +import App from './App.vue' +import router from './router/index.js' +import store from './store/index.js' +import 'element-plus/dist/index.css' +import ElementPlus from 'element-plus' +import './assets/index.js' +// import 'tailwindcss/tailwind.css' +createApp(App).use(store).use(router).use(ElementPlus).mount('#app') diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..c7756fb --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,25 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +const routes = [ + // { + // path: '/', + // name: 'index', + // component: () => import(/* webpackChunkName: "index" */ '../views/index.vue') + // }, + // { + // path: '/detail/:taskId', + // name: 'detail', + // component: () => import(/* webpackChunkName: "works-detail" */ '../views/detail.vue') + // } + { + path: '/', + name: 'detail', + component: () => import(/* webpackChunkName: "index" */ '../views/detail.vue') + }, +] + +const router = createRouter({ + history: createWebHashHistory(), + routes +}) + +export default router diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..7f5b89c --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,14 @@ +import { createStore } from 'vuex' + +export default createStore({ + state: { + }, + getters: { + }, + mutations: { + }, + actions: { + }, + modules: { + } +}) diff --git a/src/styles/css/common.css b/src/styles/css/common.css new file mode 100644 index 0000000..b41bcc2 --- /dev/null +++ b/src/styles/css/common.css @@ -0,0 +1,69 @@ + +.text-overflow-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* // 文字溢出多行 */ +.text-overflow-multi-line { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +* { + /* // text-shadow: 4px 4px 4px rgb(230, 6, 6); */ +} + +/* 美化滚动条 */ +::-webkit-scrollbar { + width: 4px; +} + +::-webkit-scrollbar-thumb { + background: #000000ad; + border-radius: 4px; + cursor: pointer; + /* // &:hover { + // background: #252525; + // } */ +} + +::-webkit-scrollbar-track { + background: #c1c1c12b; + border-radius: 4px; +} + +.one-ell { + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.two-ell { + display: -webkit-box ; + -webkit-line-clamp: 2 ; + -webkit-box-orient: vertical ; + overflow: hidden ; +} + +.three-ell { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.four-ell { + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; +} +/* // *{ +// color:#f00 !important; +// } */ \ No newline at end of file diff --git a/src/styles/css/global.scss b/src/styles/css/global.scss new file mode 100644 index 0000000..ac8026a --- /dev/null +++ b/src/styles/css/global.scss @@ -0,0 +1,12 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +@import "./transition.css"; +@import "./reset.css"; +@import "./iconfont.css"; +@import "./common.css"; +html, +body, +#app { + min-height: 100vh; +} diff --git a/src/styles/css/iconfont.css b/src/styles/css/iconfont.css new file mode 100644 index 0000000..da08a68 --- /dev/null +++ b/src/styles/css/iconfont.css @@ -0,0 +1 @@ +@import url(//at.alicdn.com/t/c/font_5111335_mdz5ib2itiq.css); \ No newline at end of file diff --git a/src/styles/css/reset.css b/src/styles/css/reset.css new file mode 100644 index 0000000..8a8eccd --- /dev/null +++ b/src/styles/css/reset.css @@ -0,0 +1,384 @@ +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; /* 1 */ + border-width: 0; /* 2 */ + border-style: solid; /* 2 */ + border-color: currentColor; /* 2 */ + /* word-break: keep-all; */ + word-break: break-all; + /* text-align: left; */ +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +*/ + +html { + line-height: 1.5; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + -moz-tab-size: 4; /* 3 */ + tab-size: 4; /* 3 */ + font-family: + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + "Helvetica Neue", + Arial, + "Noto Sans", + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji"; /* 4 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; /* 1 */ + line-height: inherit; /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; /* 1 */ + color: inherit; /* 2 */ + border-top-width: 1px; /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + "Liberation Mono", "Courier New", monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; /* 1 */ + border-color: inherit; /* 2 */ + border-collapse: collapse; /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: inherit; /* 1 */ + color: inherit; /* 1 */ + margin: 0; /* 2 */ + padding: 0; /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; /* 1 */ + /* background-color: transparent; 2 */ + background-image: none; /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::placeholder, +textarea::placeholder { + opacity: 1; /* 1 */ + color: #9ca3af; /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; /* 1 */ + vertical-align: middle; /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* +Ensure the default browser behavior of the `hidden` attribute. +*/ + +[hidden] { + display: none; +} + +.dark { + color-scheme: dark; +} diff --git a/src/styles/css/transition.css b/src/styles/css/transition.css new file mode 100644 index 0000000..1848b25 --- /dev/null +++ b/src/styles/css/transition.css @@ -0,0 +1,82 @@ +/* fade */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.3s ease-in-out; +} +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} + +/* fade-slide */ +.fade-slide-leave-active, +.fade-slide-enter-active { + transition: all 0.3s; +} +.fade-slide-enter-from { + opacity: 0; + transform: translateX(-30px); +} +.fade-slide-leave-to { + opacity: 0; + transform: translateX(30px); +} + +/* fade-bottom */ +.fade-bottom-enter-active, +.fade-bottom-leave-active { + transition: + opacity 0.25s, + transform 0.3s; +} +.fade-bottom-enter-from { + opacity: 0; + transform: translateY(-10%); +} +.fade-bottom-leave-to { + opacity: 0; + transform: translateY(10%); +} + +/* fade-scale */ +.fade-scale-leave-active, +.fade-scale-enter-active { + transition: all 0.28s; +} +.fade-scale-enter-from { + opacity: 0; + transform: scale(1.2); +} +.fade-scale-leave-to { + opacity: 0; + transform: scale(0.8); +} + +/* zoom-fade */ +.zoom-fade-enter-active, +.zoom-fade-leave-active { + transition: + transform 0.2s, + opacity 0.3s ease-out; +} +.zoom-fade-enter-from { + opacity: 0; + transform: scale(0.92); +} +.zoom-fade-leave-to { + opacity: 0; + transform: scale(1.06); +} + +/* zoom-out */ +.zoom-out-enter-active, +.zoom-out-leave-active { + transition: + opacity 0.1s ease-in-out, + transform 0.15s ease-out; +} +.zoom-out-enter-from, +.zoom-out-leave-to { + opacity: 0; + transform: scale(0); +} diff --git a/src/typings/global.d.ts b/src/typings/global.d.ts new file mode 100644 index 0000000..a20f6c2 --- /dev/null +++ b/src/typings/global.d.ts @@ -0,0 +1,79 @@ +/* Menu */ +declare namespace Menu { + interface MenuOptions { + path: string; + name: string; + component?: string | (() => Promise); + redirect?: string; + meta: MetaProps; + children?: MenuOptions[]; + } + interface MetaProps { + icon: string; + title: string; + activeMenu?: string; + isLink?: string; + isHide: boolean; + isFull: boolean; + isAffix: boolean; + isKeepAlive: boolean; + } +} + +/* FileType */ +declare namespace File { + type ImageMimeType = + | "image/apng" + | "image/bmp" + | "image/gif" + | "image/jpeg" + | "image/pjpeg" + | "image/png" + | "image/svg+xml" + | "image/tiff" + | "image/webp" + | "image/x-icon"; + + type ExcelMimeType = "application/vnd.ms-excel" | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; +} + +/* Vite */ +declare type Recordable = Record; + +declare interface ViteEnv { + VITE_USER_NODE_ENV: "development" | "production" | "test"; + VITE_GLOB_APP_TITLE: string; + VITE_PORT: number; + VITE_OPEN: boolean; + VITE_REPORT: boolean; + VITE_ROUTER_MODE: "hash" | "history"; + VITE_BUILD_COMPRESS: "gzip" | "brotli" | "gzip,brotli" | "none"; + VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean; + VITE_DROP_CONSOLE: boolean; + VITE_PWA: boolean; + VITE_DEVTOOLS: boolean; + VITE_PUBLIC_PATH: string; + VITE_API_URL: string; + VITE_BASE_PATH: string; + VITE_PROXY: [string, string, string][]; + VITE_CODEINSPECTOR: boolean; +} + +interface ImportMetaEnv extends ViteEnv { + __: unknown; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} + +/* __APP_INFO__ */ +declare const __APP_INFO__: { + pkg: { + name: string; + version: string; + dependencies: Recordable; + devDependencies: Recordable; + }; + lastBuildTime: string; +}; diff --git a/src/typings/utils.d.ts b/src/typings/utils.d.ts new file mode 100644 index 0000000..8476216 --- /dev/null +++ b/src/typings/utils.d.ts @@ -0,0 +1,17 @@ +type ObjToKeyValUnion = { + [K in keyof T]: { key: K; value: T[K] }; +}[keyof T]; + +type ObjToKeyValArray = { + [K in keyof T]: [K, T[K]]; +}[keyof T]; + +type ObjToSelectedValueUnion = { + [K in keyof T]: T[K]; +}[keyof T]; + +type Optional = Omit & Partial>; + +type GetOptional = { + [P in keyof T as T[P] extends Required[P] ? never : P]: T[P]; +}; diff --git a/src/typings/window.d.ts b/src/typings/window.d.ts new file mode 100644 index 0000000..1582726 --- /dev/null +++ b/src/typings/window.d.ts @@ -0,0 +1,8 @@ +declare global { + interface Navigator { + msSaveOrOpenBlob: (blob: Blob, fileName: string) => void; + browserLanguage: string; + } +} + +export {}; diff --git a/src/typings/work.d.ts b/src/typings/work.d.ts new file mode 100644 index 0000000..7b2ed82 --- /dev/null +++ b/src/typings/work.d.ts @@ -0,0 +1,45 @@ +/** src/types/work.ts 瀑布流全局类型 */ +export enum MediaType { + IMAGE = 1, // 纯图片 + IMAGE_TEXT = 2,// 图文 + VIDEO = 3, // 视频 + TEXT = 4 // 纯文字 +} + +export interface WaterfallItem { + avatar?: string; + nickName?: string; + fileUrl?: string; + content?: string; + fileError?: boolean; + + _isNew?: boolean; + _avatarLoaded?: boolean; + _mediaLoaded?: boolean; + + // 动态索引签名(允许 item[字段名] 取值) + [key: string]: any; +} + +export interface WaterfallProps { + list?: WaterfallItem[]; + columns?: number|string; + columnGap?: number|string; + cardGap?: number|string; + imageField?: string; + titleField?: string; + descField?: string; + placeholderImage?: string; + type?: MediaType; + hasSlot?: boolean; +} +export interface TaskInfo { + title: string; + [key: string]: any; +} + +export interface DictOption { + label: string; + value: number | string; + [key: string]: any; +} \ No newline at end of file diff --git a/src/views/detail-nosearch.vue b/src/views/detail-nosearch.vue new file mode 100644 index 0000000..a5ddde9 --- /dev/null +++ b/src/views/detail-nosearch.vue @@ -0,0 +1,320 @@ + + + + + \ No newline at end of file diff --git a/src/views/detail-open.vue b/src/views/detail-open.vue new file mode 100644 index 0000000..39804ea --- /dev/null +++ b/src/views/detail-open.vue @@ -0,0 +1,564 @@ + + + + + \ No newline at end of file diff --git a/src/views/detail.vue b/src/views/detail.vue new file mode 100644 index 0000000..82993bc --- /dev/null +++ b/src/views/detail.vue @@ -0,0 +1,628 @@ + + + + + \ No newline at end of file diff --git a/src/views/index.vue b/src/views/index.vue new file mode 100644 index 0000000..d2007b4 --- /dev/null +++ b/src/views/index.vue @@ -0,0 +1,382 @@ + + + + + \ No newline at end of file diff --git a/src/views/waterfallFlow.vue b/src/views/waterfallFlow.vue new file mode 100644 index 0000000..b1299f1 --- /dev/null +++ b/src/views/waterfallFlow.vue @@ -0,0 +1,387 @@ + + + + + \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..5804c0a --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,10 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./src/**/*.{vue,js,ts,jsx,tsx}" + ], + theme: { + extend: {}, + }, + plugins: [], + } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5295dbd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* 模块解析 */ + "moduleResolution": "bundler", // Vite 推荐 bundler + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* 别名 */ + "baseUrl": "./", + "paths": { + "@/utils/*": ["utils/*"], + "@/*": ["src/*"] + }, + + /* 类型 */ + "types": ["vite/client"], + "strict": false, + "esModuleInterop": true + }, + "include": ["src/**/*", "src/typings/**/*", "utils/**/*"], + "references": [{ "path": "./tsconfig.node.json" }] + } \ No newline at end of file diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..c4a351b --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] + } \ No newline at end of file diff --git a/utils/color.ts b/utils/color.ts new file mode 100644 index 0000000..88ebaac --- /dev/null +++ b/utils/color.ts @@ -0,0 +1,59 @@ +import { ElMessage } from "element-plus"; + +/** + * @description hex颜色转rgb颜色 + * @param {String} str 颜色值字符串 + * @returns {String} 返回处理后的颜色值 + */ +export function hexToRgb(str: any) { + let hexs: any = ""; + let reg = /^\#?[0-9A-Fa-f]{6}$/; + if (!reg.test(str)) return ElMessage.warning("输入错误的hex"); + str = str.replace("#", ""); + hexs = str.match(/../g); + for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16); + return hexs; +} + +/** + * @description rgb颜色转Hex颜色 + * @param {*} r 代表红色 + * @param {*} g 代表绿色 + * @param {*} b 代表蓝色 + * @returns {String} 返回处理后的颜色值 + */ +export function rgbToHex(r: any, g: any, b: any) { + let reg = /^\d{1,3}$/; + if (!reg.test(r) || !reg.test(g) || !reg.test(b)) return ElMessage.warning("输入错误的rgb颜色值"); + let hexs = [r.toString(16), g.toString(16), b.toString(16)]; + for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`; + return `#${hexs.join("")}`; +} + +/** + * @description 加深颜色值 + * @param {String} color 颜色值字符串 + * @param {Number} level 加深的程度,限0-1之间 + * @returns {String} 返回处理后的颜色值 + */ +export function getDarkColor(color: string, level: number) { + let reg = /^\#?[0-9A-Fa-f]{6}$/; + if (!reg.test(color)) return ElMessage.warning("输入错误的hex颜色值"); + let rgb = hexToRgb(color); + for (let i = 0; i < 3; i++) rgb[i] = Math.round(20.5 * level + rgb[i] * (1 - level)); + return rgbToHex(rgb[0], rgb[1], rgb[2]); +} + +/** + * @description 变浅颜色值 + * @param {String} color 颜色值字符串 + * @param {Number} level 加深的程度,限0-1之间 + * @returns {String} 返回处理后的颜色值 + */ +export function getLightColor(color: string, level: number) { + let reg = /^\#?[0-9A-Fa-f]{6}$/; + if (!reg.test(color)) return ElMessage.warning("输入错误的hex颜色值"); + let rgb = hexToRgb(color); + for (let i = 0; i < 3; i++) rgb[i] = Math.round(255 * level + rgb[i] * (1 - level)); + return rgbToHex(rgb[0], rgb[1], rgb[2]); +} diff --git a/utils/copyText.js b/utils/copyText.js new file mode 100644 index 0000000..ab8ccd2 --- /dev/null +++ b/utils/copyText.js @@ -0,0 +1,23 @@ +/**一键复制 */ +// import { Message } from "element-ui"; +import {showMsg} from "@/utils/showMsg"; +function fallbackCopyToClipboard(text) { + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + document.body.appendChild(textarea); + textarea.select(); + + try { + document.execCommand("copy"); + // alert('复制成功(降级方案)!'); + showMsg("已复制到剪切板", "success"); + } catch (err) { + console.error("降级复制失败:", err); + // alert('复制失败,请手动复制!'); + showMsg("复制失败,请手动复制!", "error"); + } + + document.body.removeChild(textarea); +} +export default fallbackCopyToClipboard; diff --git a/utils/date.ts b/utils/date.ts new file mode 100644 index 0000000..20af39b --- /dev/null +++ b/utils/date.ts @@ -0,0 +1,95 @@ +/** + * @description 时间相关工具函数 + */ + +/** + * 格式化日期时间为指定格式 + * @param date 日期对象或字符串 + * @param format 格式字符串,默认为 'YYYY-MM-DD HH:mm:ss' + * @returns 格式化后的日期时间字符串 + */ +export function formatDateTime(date: Date | string | number, format: string = 'YYYY-MM-DD HH:mm:ss'): string { + if (!date) return ''; + + const d = new Date(date); + + // 检查日期是否有效 + if (isNaN(d.getTime())) return ''; + + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const hours = String(d.getHours()).padStart(2, '0'); + const minutes = String(d.getMinutes()).padStart(2, '0'); + const seconds = String(d.getSeconds()).padStart(2, '0'); + + return format + .replace('YYYY', String(year)) + .replace('MM', month) + .replace('DD', day) + .replace('HH', hours) + .replace('mm', minutes) + .replace('ss', seconds); +} + +/** + * 格式化 sendTime 字段,确保为 'YYYY-MM-DD HH:mm:ss' 格式 + * @param sendTime sendTime 字段值 + * @returns 格式化后的 sendTime 字符串 + */ +export function formatSendTime(sendTime: string | Date | number): string { + if (!sendTime) return ''; + + // 如果已经是字符串格式,验证是否符合预期格式 + if (typeof sendTime === 'string') { + // 检查是否已经是 'YYYY-MM-DD HH:mm:ss' 格式 + const dateTimeRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/; + if (dateTimeRegex.test(sendTime)) { + return sendTime; + } + + // 如果是其他格式的字符串,尝试解析 + const parsed = new Date(sendTime); + if (!isNaN(parsed.getTime())) { + return formatDateTime(parsed); + } + + return ''; + } + + // 如果是 Date 对象或时间戳 + return formatDateTime(sendTime); +} + +/** + * 验证 sendTime 格式是否正确 + * @param sendTime sendTime 字段值 + * @returns 是否为有效格式 + */ +export function isValidSendTime(sendTime: string): boolean { + if (!sendTime) return false; + + const dateTimeRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/; + if (!dateTimeRegex.test(sendTime)) return false; + + // 进一步验证日期时间是否有效 + const date = new Date(sendTime); + return !isNaN(date.getTime()); +} + +/** + * 获取当前时间的 sendTime 格式 + * @returns 当前时间的格式化字符串 + */ +export function getCurrentSendTime(): string { + return formatDateTime(new Date()); +} + +/** + * 将日期时间转换为 sendTime 格式(用于表单提交前的处理) + * @param dateTime 日期时间值 + * @returns sendTime 格式字符串 + */ +export function toSendTimeFormat(dateTime: any): string { + return formatSendTime(dateTime); +} diff --git a/utils/dict.ts b/utils/dict.ts new file mode 100644 index 0000000..7eb30a5 --- /dev/null +++ b/utils/dict.ts @@ -0,0 +1,17 @@ +// ? 系统全局字典 + +/** + * @description:用户性别 + */ +export const genderType = [ + { label: "男", value: 1 }, + { label: "女", value: 2 } +]; + +/** + * @description:用户状态 + */ +export const userStatus = [ + { label: "启用", value: 1, tagType: "success" }, + { label: "禁用", value: 0, tagType: "danger" } +]; diff --git a/utils/errorHandler.ts b/utils/errorHandler.ts new file mode 100644 index 0000000..180b8c7 --- /dev/null +++ b/utils/errorHandler.ts @@ -0,0 +1,27 @@ +import { ElNotification } from "element-plus"; + +/** + * @description 全局代码错误捕捉 + * */ +const errorHandler = (error: any) => { + // 过滤 HTTP 请求错误 + if (error.status || error.status == 0) return false; + let errorMap: { [key: string]: string } = { + InternalError: "Javascript引擎内部错误", + ReferenceError: "未找到对象", + TypeError: "使用了错误的类型或对象", + RangeError: "使用内置对象时,参数超范围", + SyntaxError: "语法错误", + EvalError: "错误的使用了Eval", + URIError: "URI错误" + }; + let errorName = errorMap[error.name] || `错误码:${error.code}` || "未知错误"; + ElNotification({ + title: errorName, + message: error.message ? error.message : error, + type: "error", + duration: 3000 + }); +}; + +export default errorHandler; diff --git a/utils/index.ts b/utils/index.ts new file mode 100644 index 0000000..f65a7bc --- /dev/null +++ b/utils/index.ts @@ -0,0 +1,470 @@ +import { isArray } from "@/utils/is"; +import * as dateUtils from "@/utils/date"; + +const mode = import.meta.env.VITE_ROUTER_MODE; + +/** + * @description 获取localStorage + * @param {String} key Storage名称 + * @returns {String} + */ +export function localGet(key: string) { + const value = window.localStorage.getItem(key); + try { + return JSON.parse(window.localStorage.getItem(key) as string); + } catch (error) { + console.error(error); + return value; + } +} + +/** + * @description 存储localStorage + * @param {String} key Storage名称 + * @param {*} value Storage值 + * @returns {void} + */ +export function localSet(key: string, value: any) { + window.localStorage.setItem(key, JSON.stringify(value)); +} + +/** + * @description 清除localStorage + * @param {String} key Storage名称 + * @returns {void} + */ +export function localRemove(key: string) { + window.localStorage.removeItem(key); +} + +/** + * @description 清除所有localStorage + * @returns {void} + */ +export function localClear() { + window.localStorage.clear(); +} + +/** + * @description 判断数据类型 + * @param {*} val 需要判断类型的数据 + * @returns {String} + */ +export function isType(val: any) { + if (val === null) return "null"; + if (typeof val !== "object") return typeof val; + else return Object.prototype.toString.call(val).slice(8, -1).toLocaleLowerCase(); +} + +/** + * @description 生成唯一 uuid + * @returns {String} + */ +export function generateUUID() { + let uuid = ""; + for (let i = 0; i < 32; i++) { + let random = (Math.random() * 16) | 0; + if (i === 8 || i === 12 || i === 16 || i === 20) uuid += "-"; + uuid += (i === 12 ? 4 : i === 16 ? (random & 3) | 8 : random).toString(16); + } + return uuid; +} + +/** + * 判断两个对象是否相同 + * @param {Object} a 要比较的对象一 + * @param {Object} b 要比较的对象二 + * @returns {Boolean} 相同返回 true,反之 false + */ +export function isObjectValueEqual(a: { [key: string]: any }, b: { [key: string]: any }) { + if (!a || !b) return false; + let aProps = Object.getOwnPropertyNames(a); + let bProps = Object.getOwnPropertyNames(b); + if (aProps.length != bProps.length) return false; + for (let i = 0; i < aProps.length; i++) { + let propName = aProps[i]; + let propA = a[propName]; + let propB = b[propName]; + if (!b.hasOwnProperty(propName)) return false; + if (propA instanceof Object) { + if (!isObjectValueEqual(propA, propB)) return false; + } else if (propA !== propB) { + return false; + } + } + return true; +} + +/** + * @description 生成随机数 + * @param {Number} min 最小值 + * @param {Number} max 最大值 + * @returns {Number} + */ +export function randomNum(min: number, max: number): number { + let num = Math.floor(Math.random() * (min - max) + max); + return num; +} + +/** + * @description 获取当前时间对应的提示语 + * @returns {String} + */ +export function getTimeState() { + let timeNow = new Date(); + let hours = timeNow.getHours(); + if (hours >= 6 && hours <= 10) return `早上好 ⛅`; + if (hours >= 10 && hours <= 14) return `中午好 🌞`; + if (hours >= 14 && hours <= 18) return `下午好 🌞`; + if (hours >= 18 && hours <= 24) return `晚上好 🌛`; + if (hours >= 0 && hours <= 6) return `凌晨好 🌛`; +} + +/** + * @description 获取浏览器默认语言 + * @returns {String} + */ +export function getBrowserLang() { + let browserLang = navigator.language ? navigator.language : navigator.browserLanguage; + let defaultBrowserLang = ""; + if (["cn", "zh", "zh-cn"].includes(browserLang.toLowerCase())) { + defaultBrowserLang = "zh"; + } else { + defaultBrowserLang = "en"; + } + return defaultBrowserLang; +} + +/** + * @description 获取不同路由模式所对应的 url + params + * @returns {String} + */ +export function getUrlWithParams() { + const url = { + hash: location.hash.substring(1), + history: location.pathname + location.search + }; + return url[mode]; +} + +/** + * @description 使用递归扁平化菜单,方便添加动态路由 + * @param {Array} menuList 菜单列表 + * @returns {Array} + */ +export function getFlatMenuList(menuList: Menu.MenuOptions[]): Menu.MenuOptions[] { + let newMenuList: Menu.MenuOptions[] = JSON.parse(JSON.stringify(menuList)); + return newMenuList.flatMap(item => [item, ...(item.children ? getFlatMenuList(item.children) : [])]); +} + +/** + * @description 使用递归过滤出需要渲染在左侧菜单的列表 (需剔除 isHide == true 的菜单) + * @param {Array} menuList 菜单列表 + * @returns {Array} + * */ +export function getShowMenuList(menuList: Menu.MenuOptions[]) { + let newMenuList: Menu.MenuOptions[] = JSON.parse(JSON.stringify(menuList)); + return newMenuList.filter(item => { + item.children?.length && (item.children = getShowMenuList(item.children)); + return !item.meta?.isHide; + }); +} + +/** + * @description 使用递归找出所有面包屑存储到 pinia/vuex 中 + * @param {Array} menuList 菜单列表 + * @param {Array} parent 父级菜单 + * @param {Object} result 处理后的结果 + * @returns {Object} + */ +export const getAllBreadcrumbList = (menuList: Menu.MenuOptions[], parent = [], result: { [key: string]: any } = {}) => { + for (const item of menuList) { + result[item.path] = [...parent, item]; + if (item.children) getAllBreadcrumbList(item.children, result[item.path], result); + } + return result; +}; + +/** + * @description 使用递归处理路由菜单 path,生成一维数组 (第一版本地路由鉴权会用到,该函数暂未使用) + * @param {Array} menuList 所有菜单列表 + * @param {Array} menuPathArr 菜单地址的一维数组 ['**','**'] + * @returns {Array} + */ +export function getMenuListPath(menuList: Menu.MenuOptions[], menuPathArr: string[] = []): string[] { + for (const item of menuList) { + if (typeof item === "object" && item.path) menuPathArr.push(item.path); + if (item.children?.length) getMenuListPath(item.children, menuPathArr); + } + return menuPathArr; +} + +/** + * @description 递归查询当前 path 所对应的菜单对象 (该函数暂未使用) + * @param {Array} menuList 菜单列表 + * @param {String} path 当前访问地址 + * @returns {Object | null} + */ +export function findMenuByPath(menuList: Menu.MenuOptions[], path: string): Menu.MenuOptions | null { + for (const item of menuList) { + if (item.path === path) return item; + if (item.children) { + const res = findMenuByPath(item.children, path); + if (res) return res; + } + } + return null; +} + +/** + * @description 使用递归过滤需要缓存的菜单 name (该函数暂未使用) + * @param {Array} menuList 所有菜单列表 + * @param {Array} keepAliveNameArr 缓存的菜单 name ['**','**'] + * @returns {Array} + * */ +export function getKeepAliveRouterName(menuList: Menu.MenuOptions[], keepAliveNameArr: string[] = []) { + menuList.forEach(item => { + item.meta.isKeepAlive && item.name && keepAliveNameArr.push(item.name); + item.children?.length && getKeepAliveRouterName(item.children, keepAliveNameArr); + }); + return keepAliveNameArr; +} + +/** + * @description 格式化表格单元格默认值 (el-table-column) + * @param {Number} row 行 + * @param {Number} col 列 + * @param {*} callValue 当前单元格值 + * @returns {String} + * */ +export function formatTableColumn(row: number, col: number, callValue: any) { + // 如果当前值为数组,使用 / 拼接(根据需求自定义) + if (isArray(callValue)) return callValue.length ? callValue.join(" / ") : "--"; + return callValue ?? "--"; +} + +/** + * @description 处理 ProTable 值为数组 || 无数据 + * @param {*} callValue 需要处理的值 + * @returns {String} + * */ +export function formatValue(callValue: any) { + // 如果当前值为数组,使用 / 拼接(根据需求自定义) + if (isArray(callValue)) return callValue.length ? callValue.join(" / ") : "--"; + return callValue ?? "--"; +} + +/** + * @description 处理 prop 为多级嵌套的情况,返回的数据 (列如: prop: user.name) + * @param {Object} row 当前行数据 + * @param {String} prop 当前 prop + * @returns {*} + * */ +export function handleRowAccordingToProp(row: { [key: string]: any }, prop: string) { + if (!prop.includes(".")) return row[prop] ?? "--"; + prop.split(".").forEach(item => (row = row[item] ?? "--")); + return row; +} + +/** + * @description 处理 prop,当 prop 为多级嵌套时 ==> 返回最后一级 prop + * @param {String} prop 当前 prop + * @returns {String} + * */ +export function handleProp(prop: string) { + const propArr = prop.split("."); + if (propArr.length == 1) return prop; + return propArr[propArr.length - 1]; +} + + +/** + * @description 递归查找 callValue 对应的 enum 值 + * */ +export function findItemNested(enumData: any, callValue: any, value: string, children: string) { + return enumData.reduce((accumulator: any, current: any) => { + if (accumulator) return accumulator; + if (current[value] === callValue) return current; + if (current[children]) return findItemNested(current[children], callValue, value, children); + }, null); +} + +export function checkType(data: unknown): string { + const type = Object.prototype.toString.call(data); + return type.slice(8, -1).toLowerCase(); +} + +export function isEmpty(data: unknown): boolean { + const dataType = checkType(data); + switch (dataType) { + case "array": + return !(data as Array).length; + case "object": + return !Object.keys(data as object).length; + case "map": + case "set": + return !(data as Map | Set).size; + case "boolean": + case "number": + case "symbol": + case "function": + return false; + default: + return !data; + } +} + +import type { FilesType } from "../components/FilesCard/types.d.ts"; + +/* 公共 相关 开始 */ +/* 公共 相关 结束 */ + +/* FileCard 组件相关 开始 */ +// 更据文件后缀名获取文件类型 +export function getFileType(fileExtension: string): { + lowerCase: FilesType; + upperCase: string; +} { + // 去除后缀名开头的点,并转换为小写 + const cleanExtension = fileExtension.replace(".", "").toLowerCase(); + if (!cleanExtension) { + return { lowerCase: "unknown", upperCase: "Unknown" }; + } + const imageExtensions = ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"]; + const wordExtensions = ["doc", "docx"]; + const excelExtensions = ["xls", "xlsx"]; + const pptExtensions = ["ppt", "pptx"]; + const audioExtensions = ["mp3", "wav", "ogg", "flac"]; + const videoExtensions = ["mp4", "avi", "mov", "mkv"]; + const codeExtensions = ["js", "ts", "html", "css", "py", "java", "c", "cpp", "json", "php"]; + const databaseExtensions = ["sql", "db", "sqlite"]; + const zipExtensions = ["zip", "rar", "7z"]; + const markExtensions = ["md", "mdx"]; + + if (imageExtensions.includes(cleanExtension)) { + return { lowerCase: "image", upperCase: "Image" }; + } + if (wordExtensions.includes(cleanExtension)) { + return { lowerCase: "word", upperCase: "Word" }; + } + if (excelExtensions.includes(cleanExtension)) { + return { lowerCase: "excel", upperCase: "Excel" }; + } + if (pptExtensions.includes(cleanExtension)) { + return { lowerCase: "ppt", upperCase: "Ppt" }; + } + if (cleanExtension === "pdf") { + return { lowerCase: "pdf", upperCase: "Pdf" }; + } + if (cleanExtension === "txt") { + return { lowerCase: "txt", upperCase: "Txt" }; + } + if (markExtensions.includes(cleanExtension)) { + return { lowerCase: "mark", upperCase: "Markdown" }; + } + if (audioExtensions.includes(cleanExtension)) { + return { lowerCase: "audio", upperCase: "Audio" }; + } + if (videoExtensions.includes(cleanExtension)) { + return { lowerCase: "video", upperCase: "Video" }; + } + if (codeExtensions.includes(cleanExtension)) { + return { lowerCase: "code", upperCase: "Code" }; + } + if (databaseExtensions.includes(cleanExtension)) { + return { lowerCase: "database", upperCase: "Database" }; + } + if (cleanExtension === "lnk") { + return { lowerCase: "link", upperCase: "Link" }; + } + if (zipExtensions.includes(cleanExtension)) { + return { lowerCase: "zip", upperCase: "Zip" }; + } + if (cleanExtension === "obj" || cleanExtension === "fbx" || cleanExtension === "glb") { + return { lowerCase: "three", upperCase: "3D" }; + } + return { lowerCase: "file", upperCase: "File" }; +} + +// 获取文件大小 +export function getSize(size: number) { + let retSize = size; + const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]; + let unitIndex = 0; + + while (retSize >= 1024 && unitIndex < units.length - 1) { + retSize /= 1024; + unitIndex++; + } + + return `${retSize.toFixed(0)} ${units[unitIndex]}`; +} + +// 通过文件流,生成图片预览 +// Follow code is copy from `antd/components/upload/utils.ts`: +export function isImageFileType(type: string): boolean { + return type.indexOf("image/") === 0; +} +const MEASURE_SIZE = 200; +export function previewImage(file: File | Blob): Promise { + return new Promise(resolve => { + if (!file || !file.type || !isImageFileType(file.type)) { + resolve(""); + return; + } + + const img = new Image(); + img.onload = () => { + const { width, height } = img; + + const ratio = width / height; + const MEASURE_SIZE_WIDTH = ratio > 1 ? MEASURE_SIZE : MEASURE_SIZE * ratio; + const MEASURE_SIZE_HEIGHT = ratio > 1 ? MEASURE_SIZE / ratio : MEASURE_SIZE; + + const canvas = document.createElement("canvas"); + canvas.width = MEASURE_SIZE_WIDTH; + canvas.height = MEASURE_SIZE_HEIGHT; + canvas.style.cssText = `position: fixed; left: 0; top: 0; width: ${MEASURE_SIZE_WIDTH}px; height: ${MEASURE_SIZE_HEIGHT}px; z-index: 9999; display: none;`; + document.body.appendChild(canvas); + const ctx = canvas.getContext("2d"); + + ctx!.drawImage(img, 0, 0, MEASURE_SIZE_WIDTH, MEASURE_SIZE_HEIGHT); + const dataURL = canvas.toDataURL(); + document.body.removeChild(canvas); + window.URL.revokeObjectURL(img.src); + resolve(dataURL); + }; + img.crossOrigin = "anonymous"; + if (file.type.startsWith("image/svg+xml")) { + const reader = new FileReader(); + reader.onload = () => { + if (reader.result && typeof reader.result === "string") { + img.src = reader.result; + } + }; + reader.readAsDataURL(file); + } else if (file.type.startsWith("image/gif")) { + const reader = new FileReader(); + reader.onload = () => { + if (reader.result) { + resolve(reader.result as string); + } + }; + reader.readAsDataURL(file); + } else { + img.src = window.URL.createObjectURL(file); + } + }); +} + +/* FileCard 组件相关 结束 */ + +/* 日期时间工具函数 开始 */ +export const { + formatDateTime, + formatSendTime, + isValidSendTime, + getCurrentSendTime, + toSendTimeFormat +} = dateUtils; +/* 日期时间工具函数 结束 */ diff --git a/utils/is/index.ts b/utils/is/index.ts new file mode 100644 index 0000000..5678491 --- /dev/null +++ b/utils/is/index.ts @@ -0,0 +1,125 @@ +/** + * @description: 判断值是否未某个类型 + */ +export function is(val: unknown, type: string) { + return Object.prototype.toString.call(val) === `[object ${type}]`; +} + +/** + * @description: 是否为函数 + */ +export function isFunction(val: unknown): val is T { + return is(val, "Function"); +} + +/** + * @description: 是否已定义 + */ +export const isDef = (val?: T): val is T => { + return typeof val !== "undefined"; +}; + +/** + * @description: 是否未定义 + */ +export const isUnDef = (val?: T): val is T => { + return !isDef(val); +}; + +/** + * @description: 是否为对象 + */ +export const isObject = (val: any): val is Record => { + return val !== null && is(val, "Object"); +}; + +/** + * @description: 是否为时间 + */ +export function isDate(val: unknown): val is Date { + return is(val, "Date"); +} + +/** + * @description: 是否为数值 + */ +export function isNumber(val: unknown): val is number { + return is(val, "Number"); +} + +/** + * @description: 是否为AsyncFunction + */ +export function isAsyncFunction(val: unknown): val is Promise { + return is(val, "AsyncFunction"); +} + +/** + * @description: 是否为promise + */ +export function isPromise(val: unknown): val is Promise { + return is(val, "Promise") && isObject(val) && isFunction(val.then) && isFunction(val.catch); +} + +/** + * @description: 是否为字符串 + */ +export function isString(val: unknown): val is string { + return is(val, "String"); +} + +/** + * @description: 是否为boolean类型 + */ +export function isBoolean(val: unknown): val is boolean { + return is(val, "Boolean"); +} + +/** + * @description: 是否为数组 + */ +export function isArray(val: any): val is Array { + return val && Array.isArray(val); +} + +/** + * @description: 是否客户端 + */ +export const isClient = () => { + return typeof window !== "undefined"; +}; + +/** + * @description: 是否为浏览器 + */ +export const isWindow = (val: any): val is Window => { + return typeof window !== "undefined" && is(val, "Window"); +}; + +/** + * @description: 是否为 element 元素 + */ +export const isElement = (val: unknown): val is Element => { + return isObject(val) && !!val.tagName; +}; + +/** + * @description: 是否为 null + */ +export function isNull(val: unknown): val is null { + return val === null; +} + +/** + * @description: 是否为 null || undefined + */ +export function isNullOrUnDef(val: unknown): val is null | undefined { + return isUnDef(val) || isNull(val); +} + +/** + * @description: 是否为 16 进制颜色 + */ +export const isHexColor = (str: string) => { + return /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(str); +}; diff --git a/utils/loading.js b/utils/loading.js new file mode 100644 index 0000000..206e2e4 --- /dev/null +++ b/utils/loading.js @@ -0,0 +1,30 @@ +import { ElLoading } from "element-plus"; + +const loadingStack = []; + +const showLoading = (text='数据加载中...') => { + const loadingInstance = ElLoading.service({ + lock: true, + text: `${text}...`, + background: "rgba(228, 228, 228, 0.57)", + }); + loadingStack.push(loadingInstance); + return loadingInstance; +}; + +const hiddenLoading = () => { + if (loadingStack.length > 0) { + const loadingInstance = loadingStack.pop(); + loadingInstance.close(); + } +}; + +// 强制关闭所有 Loading(可选) +const hiddenAllLoading = () => { + while (loadingStack.length > 0) { + const loadingInstance = loadingStack.pop(); + loadingInstance.close(); + } +}; + +export { showLoading, hiddenLoading, hiddenAllLoading }; diff --git a/utils/showMsg.ts b/utils/showMsg.ts new file mode 100644 index 0000000..1ffdbb5 --- /dev/null +++ b/utils/showMsg.ts @@ -0,0 +1,13 @@ +// 显示错误 +import { ElNotification } from "element-plus"; +export function showMsg(t: string, type = "error", isCatch = true, reason?: string, duration = 2000) { + ElNotification({ + title: t, + type, + duration + }); + if (isCatch && type != "success") { + throw Error(reason || t); + } + // if (isCatch && type != 'success') { return reason || t; } +} diff --git a/utils/svg.ts b/utils/svg.ts new file mode 100644 index 0000000..0c191c9 --- /dev/null +++ b/utils/svg.ts @@ -0,0 +1,13 @@ +/** + * @description Loading Svg + */ +export const loadingSvg = ` + +`; diff --git a/utils/toBeian.js b/utils/toBeian.js new file mode 100644 index 0000000..f699daf --- /dev/null +++ b/utils/toBeian.js @@ -0,0 +1,3 @@ +export const toBeian = () => { + window.open("https://beian.miit.gov.cn/", "_blank"); +}; diff --git a/utils/useFileNameParser.ts b/utils/useFileNameParser.ts new file mode 100644 index 0000000..b83da6b --- /dev/null +++ b/utils/useFileNameParser.ts @@ -0,0 +1,25 @@ +import type { Ref } from "vue"; +import { computed } from "vue"; + +export default function useFileNameParser(name: Ref) { + const namePrefix = computed(() => { + const nameStr = name.value || ""; + const lastDotIndex = nameStr.lastIndexOf("."); + return lastDotIndex === -1 ? nameStr : nameStr.slice(0, lastDotIndex); + }); + + const nameSuffix = computed(() => { + const nameStr = name.value || ""; + const lastDotIndex = nameStr.lastIndexOf("."); + if (lastDotIndex === -1 && nameStr.length - lastDotIndex > 10) { + // 文件名长度超过10个字符 显示.file + return ".file"; + } + return lastDotIndex === -1 ? "" : nameStr.slice(lastDotIndex); + }); + + return { + namePrefix, + nameSuffix + }; +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..eefc447 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,56 @@ +import { defineConfig, ConfigEnv, loadEnv } from 'vite' +// import { +// getRootPath, +// getSrcPath, +// setupVitePlugins, +// } from "./build"; +import vue from '@vitejs/plugin-vue' +import path from 'path' +import { fileURLToPath, URL } from 'url' +// https://vitejs.dev/config/ +export default defineConfig((configEnv: ConfigEnv) => { + const viteEnv = loadEnv( + configEnv.mode, + process.cwd() + ) as unknown as ImportMetaEnv; + return { + plugins: [vue()], + // plugins: setupVitePlugins(viteEnv), + // 1. 路径别名(对应你之前的 @ -> src) + resolve: { + alias: { + '@/utils': path.resolve(__dirname, './utils'), + "~": path.resolve(__dirname, './src'), + '@': path.resolve(__dirname, './src'), + '@/typings': path.resolve(__dirname, './src/typings'), + components: path.resolve(__dirname, "components"), + + }, + extensions: ['.js', '.ts', '.jsx', '.tsx', '.json', '.vue'], + }, + + // 2. SCSS 兼容 & 全局变量(和 Vue CLI 写法不同) + css: { + preprocessorOptions: { + scss: { + // 如果你有全局 scss 变量文件,在这里引入 + // additionalData: `@use "@/assets/styles/variables.scss" as *;` + } + } + }, + + // 3. 开发服务配置(端口、跨域、自动打开浏览器等) + server: { + host: "0.0.0.0", + port: viteEnv.VITE_CLI_PORT, + proxy: { + [viteEnv.VITE_BASE_API]: { + target: viteEnv.VITE_BASE_PATH, + changeOrigin: true, + rewrite: (path: string) => + path.replace(new RegExp("^" + viteEnv.VITE_BASE_API), ""), + }, + }, + } + } +}) \ No newline at end of file