├── .env ├── .env.production ├── .env.test ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── auto-imports.d.ts ├── components.d.ts ├── docker ├── Dockerfile ├── auto_create.sql ├── base.sh ├── build-beta.sh ├── build-debian.sh ├── build-latest.sh ├── build-local.sh ├── build-version.mjs ├── client │ └── .gitkeep ├── debian.Dockerfile ├── mysql.cnf ├── nginx.conf ├── server │ ├── .env │ ├── package.json │ └── pnpm-lock.yaml ├── sources.list └── start.sh ├── docs ├── .env ├── .env.production ├── .vitepress │ ├── config.mts │ └── theme │ │ ├── bg.png │ │ ├── index.scss │ │ └── index.ts ├── author.md ├── auto-imports.d.ts ├── components.d.ts ├── deploy │ ├── design │ │ ├── api.md │ │ ├── db.md │ │ ├── index.md │ │ └── shell.md │ ├── docker.md │ ├── faq.md │ ├── index.md │ ├── local.md │ ├── online-new.md │ ├── online-v3.md │ ├── online.md │ └── qiniu.md ├── index.md ├── introduction │ ├── about │ │ ├── code.md │ │ └── index.md │ └── feature │ │ ├── admin.md │ │ └── index.md ├── plan │ ├── log.md │ ├── todo.md │ └── wish.md ├── praise │ └── index.md ├── public │ ├── favicon.ico │ ├── group.png │ ├── logo.png │ └── robots.txt ├── src │ ├── apis │ │ ├── ajax.ts │ │ ├── index.ts │ │ └── modules │ │ │ └── wish.ts │ └── components │ │ ├── Avatar.vue │ │ ├── Home.vue │ │ ├── Picture.vue │ │ ├── Praise.vue │ │ ├── WishBtn.vue │ │ ├── WishPanel.vue │ │ └── callme │ │ └── index.vue └── vite.config.mts ├── eslint.config.mjs ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.ico └── logo.png ├── scripts └── deploy │ ├── docs.mjs │ ├── prod.mjs │ └── test.mjs ├── src ├── @types │ ├── ajax.d.ts │ ├── api.d.ts │ ├── lib.d.ts │ └── page.d.ts ├── App.vue ├── apis │ ├── ajax.ts │ ├── index.ts │ └── modules │ │ ├── action.ts │ │ ├── category.ts │ │ ├── config.ts │ │ ├── file.ts │ │ ├── people.ts │ │ ├── public.ts │ │ ├── super │ │ ├── overview.ts │ │ └── user.ts │ │ ├── task.ts │ │ ├── user.ts │ │ └── wish.ts ├── assets │ ├── i │ │ └── EasyPicker.png │ ├── logo.png │ └── styles │ │ └── app.css ├── components │ ├── HomeFooter │ │ └── index.vue │ ├── HomeHeader │ │ └── index.vue │ ├── InfosForm │ │ └── index.vue │ ├── MessageList │ │ └── index.vue │ ├── MessagePanel │ │ └── index.vue │ ├── Praise │ │ └── index.vue │ ├── QrCode.vue │ ├── linkDialog.vue │ └── loginPanel.vue ├── composables │ ├── auth.ts │ ├── form.ts │ ├── index.ts │ ├── ui.ts │ └── user.ts ├── constants │ └── index.ts ├── env.d.ts ├── main.ts ├── pages │ ├── 404 │ │ └── index.vue │ ├── about │ │ └── index.vue │ ├── callme │ │ └── index.vue │ ├── dashboard │ │ ├── config │ │ │ └── index.vue │ │ ├── files │ │ │ └── index.vue │ │ ├── index.vue │ │ ├── manage │ │ │ ├── config │ │ │ │ └── index.vue │ │ │ ├── index.vue │ │ │ ├── overview │ │ │ │ └── index.vue │ │ │ ├── user │ │ │ │ └── index.vue │ │ │ └── wish │ │ │ │ └── index.vue │ │ └── tasks │ │ │ ├── components │ │ │ ├── CategoryPanel.vue │ │ │ ├── CreateTask.vue │ │ │ ├── TaskInfo.vue │ │ │ └── infoPanel │ │ │ │ ├── ddl.vue │ │ │ │ ├── file.vue │ │ │ │ ├── info.vue │ │ │ │ ├── people.vue │ │ │ │ ├── template.vue │ │ │ │ ├── tip.vue │ │ │ │ └── tipInfo.vue │ │ │ ├── index.vue │ │ │ └── public.ts │ ├── disabled │ │ └── index.vue │ ├── feedback │ │ └── index.vue │ ├── home │ │ └── index.vue │ ├── login │ │ └── index.vue │ ├── register │ │ └── index.vue │ ├── reset │ │ └── index.vue │ ├── task │ │ └── index.vue │ └── wish │ │ └── index.vue ├── router │ ├── Interceptor │ │ └── index.ts │ ├── index.ts │ └── routes │ │ └── index.ts ├── shims-vue.d.ts ├── store │ ├── index.ts │ └── modules │ │ ├── category.ts │ │ ├── task.ts │ │ └── user.ts └── utils │ ├── elementUI.ts │ ├── networkUtil.ts │ ├── other.ts │ ├── regExp.ts │ └── stringUtil.ts ├── tsconfig.json └── vite.config.mts /.env: -------------------------------------------------------------------------------- 1 | VITE_ROUTER_BASE=/ 2 | VITE_APP_AXIOS_BASE_URL=/api/ 3 | VITE_APP_TITLE=(local)EasyPicker-轻取 4 | VITE_APP_PV_PATH=localhost:3000 -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # .env.production 2 | VITE_APP_TITLE=EasyPicker-轻取 3 | VITE_APP_PV_PATH=ep2.sugarat.top/api -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # .env.test 2 | VITE_APP_TITLE=(test)EasyPicker-轻取 3 | VITE_APP_PV_PATH=ep.test.sugarat.top/api -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: prod-CI 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ release ] 10 | pull_request: 11 | types: [ assigned ] 12 | branches: [ release ] 13 | 14 | # Allows you to run this workflow manually from the Actions tab 15 | workflow_dispatch: 16 | 17 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 18 | jobs: 19 | # This workflow contains a single job called "build" 20 | build: 21 | # The type of runner that the job will run on 22 | runs-on: ubuntu-latest 23 | 24 | # Steps represent a sequence of tasks that will be executed as part of the job 25 | steps: 26 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 27 | - uses: actions/checkout@v2 28 | # 配置rsa密钥自动登陆 29 | - uses: webfactory/ssh-agent@v0.4.1 30 | with: 31 | ssh-private-key: ${{ secrets.ACCESS_TOKEN }} 32 | - name: Setup knownhosts 33 | run: ssh-keyscan ${{ secrets.REMOTE_ORIGIN }} >> ~/.ssh/known_hosts 34 | 35 | - name: Install dependence 36 | run: | 37 | echo 开始----安装依赖 38 | yarn install 39 | - name: Build 40 | run: | 41 | echo 开始----构建 42 | yarn build 43 | - name: Compress dist 44 | run: | 45 | echo 开始----压缩 46 | yarn compress 47 | # 上传压缩的内容 48 | - name: Upload package 49 | uses: easingthemes/ssh-deploy@v2.1.5 50 | env: 51 | SSH_PRIVATE_KEY: ${{ secrets.ACCESS_TOKEN }} 52 | ARGS: "-rltgoDzvO --delete" 53 | SOURCE: "ep-dev-clinet.tar.gz" 54 | REMOTE_HOST: ${{ secrets.REMOTE_ORIGIN }} 55 | REMOTE_USER: ${{ secrets.REMOTE_USER }} 56 | TARGET: ${{ secrets.TARGET }} 57 | # 部署上传的包 58 | - name: Deploy 59 | run: | 60 | echo 开始----部署 61 | yarn deploy -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | cache 7 | *.tar.gz 8 | ep_backup -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # registry=https://registry.npmmirror.com/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.inlineSuggest.showToolbar": "onHover", 3 | // Disable the default formatter, use eslint instead 4 | "prettier.enable": false, 5 | "editor.formatOnSave": false, 6 | 7 | // Auto fix 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": "explicit", 10 | "source.organizeImports": "never" 11 | }, 12 | 13 | // Silent the stylistic rules in you IDE, but still auto fix them 14 | "eslint.rules.customizations": [ 15 | { "rule": "style/*", "severity": "off" }, 16 | { "rule": "format/*", "severity": "off" }, 17 | { "rule": "*-indent", "severity": "off" }, 18 | { "rule": "*-spacing", "severity": "off" }, 19 | { "rule": "*-spaces", "severity": "off" }, 20 | { "rule": "*-order", "severity": "off" }, 21 | { "rule": "*-dangle", "severity": "off" }, 22 | { "rule": "*-newline", "severity": "off" }, 23 | { "rule": "*quotes", "severity": "off" }, 24 | { "rule": "*semi", "severity": "off" } 25 | ], 26 | 27 | // Enable eslint for all supported languages 28 | "eslint.validate": [ 29 | "javascript", 30 | "javascriptreact", 31 | "typescript", 32 | "typescriptreact", 33 | "vue", 34 | "html", 35 | "markdown", 36 | "json", 37 | "jsonc", 38 | "yaml", 39 | "toml", 40 | "xml", 41 | "gql", 42 | "graphql", 43 | "astro", 44 | "css", 45 | "less", 46 | "scss", 47 | "pcss", 48 | "postcss" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 sugar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 7 | 8 |

EasyPicker(轻取)

9 | 10 |

为提高在线文件收取效率而生

11 |

12 | 13 | Status 15 | 16 |

17 | 18 | ![](https://img.cdn.sugarat.top/mdImg/MTY3ODAwMzU3MTc2Ng==678003571766) 19 | 20 | ## 简介 21 | [在线文件收取平台](https://docs.ep.sugarat.top/) 22 | 23 | 应用开源,支持[私有化部署](https://docs.ep.sugarat.top/) 24 | 25 | ## 快速体验 26 | * [应用主页](https://ep2.sugarat.top) 27 | * [提交文件](https://ep2.sugarat.top/task/627bd3b18a567f1b47bcdace) 28 | 29 | ## 项目背景 30 | 校园学习或者工作场景中会出现以下几个场景: 31 | * 每次碰到上机课的时候,都会遇到收取实验报告。 32 | * 需要收取每个人填写的各种电子表格。 33 | * 需要通过QQ/微信等等收集各种截图 34 | * 类似场景还有不少就不列举了。。。 35 | 36 | 通常的方式是,通过QQ/微信/邮箱等收取,弊端显而易见,太过于麻烦且不方便整理统计。还占用电脑/手机内存。为了解决这个问题,此项目应运而生。 37 | 38 | 39 | 欢迎[体验](https://ep.sugarat.top)分享 40 | 41 | 42 | 43 | ## 赞赏 44 | 如果觉得项目还ok,可以请作者喝 `茶`,支持一下 45 | 46 | | 赞赏 | 加微信 | 47 | | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | 48 | |

| | 49 | 50 | 51 | 52 | ## 反馈 53 | * [问卷反馈](https://www.wenjuan.com/s/UZBZJvA040/) 54 | * [提需求](https://ep.sugarat.top/wish) 55 | 56 | ## 相关文档 57 | * [开发规划](https://docs.ep.sugarat.top/plan/todo.html) 58 | * [本地启动&线上部署指南](https://docs.ep.sugarat.top/) 59 | * [接口文档](https://easy2.w.eolink.com/share/index?shareCode=7SF9Na) 60 | * [数据库设计文档](https://github.com/ATQQ/easypicker2-server/tree/master/docs) 61 | * [更新日志](https://docs.ep.sugarat.top/plan/log.html) 62 | 63 | ## 相关地址 64 | 注:两环境数据不互通,新功能会先在测试环境进行实验 65 | 66 | 1. 正式环境: 67 | * https://ep.sugarat.top 68 | * https://ep2.sugarat.top 69 | 2. 测试环境: 70 | * https://ep.test.sugarat.top 71 | * https://ep.dev.sugarat.top 72 | 73 | ## 其它信息 74 | ### 技术栈 75 | * 前端:Vue3,Typescript,Vite - [模板仓库](https://github.com/ATQQ/vite-vue3-template) 76 | * 服务端:Typescript,Node.js - [模板仓库](https://github.com/ATQQ/node-server) 77 | ### 相关仓库 78 | #### EasyPicker1.0(已下线) 79 | 1. ~~服务端(Java-已弃用):https://github.com/ATQQ/EasyPicker~~ 80 | 2. 客户端(web) :https://github.com/ATQQ/EasyPicker-webpack 81 | 3. 服务端(Node.js):https://github.com/ATQQ/easypicker-server 82 | 83 | #### [EasyPicker2.0](https://ep2.sugarat.top) 84 | 1. 客户端(Web):https://github.com/ATQQ/easypicker2-client 85 | 2. 服务端(Node.js):https://github.com/ATQQ/easypicker2-server 86 | 87 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | // We suggest you to commit this file into source control 3 | declare global { 4 | 5 | } 6 | export {} 7 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | ElAlert: typeof import('element-plus/es')['ElAlert'] 11 | ElBadge: typeof import('element-plus/es')['ElBadge'] 12 | ElButton: typeof import('element-plus/es')['ElButton'] 13 | ElCard: typeof import('element-plus/es')['ElCard'] 14 | ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] 15 | ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] 16 | ElDialog: typeof import('element-plus/es')['ElDialog'] 17 | ElDivider: typeof import('element-plus/es')['ElDivider'] 18 | ElDropdown: typeof import('element-plus/es')['ElDropdown'] 19 | ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] 20 | ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] 21 | ElEmpty: typeof import('element-plus/es')['ElEmpty'] 22 | ElForm: typeof import('element-plus/es')['ElForm'] 23 | ElFormItem: typeof import('element-plus/es')['ElFormItem'] 24 | ElIcon: typeof import('element-plus/es')['ElIcon'] 25 | ElImage: typeof import('element-plus/es')['ElImage'] 26 | ElImageViewer: typeof import('element-plus/es')['ElImageViewer'] 27 | ElInput: typeof import('element-plus/es')['ElInput'] 28 | ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] 29 | ElLink: typeof import('element-plus/es')['ElLink'] 30 | ElMenu: typeof import('element-plus/es')['ElMenu'] 31 | ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] 32 | ElOption: typeof import('element-plus/es')['ElOption'] 33 | ElPagination: typeof import('element-plus/es')['ElPagination'] 34 | ElPopover: typeof import('element-plus/es')['ElPopover'] 35 | ElRadio: typeof import('element-plus/es')['ElRadio'] 36 | ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] 37 | ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] 38 | ElSelect: typeof import('element-plus/es')['ElSelect'] 39 | ElSwitch: typeof import('element-plus/es')['ElSwitch'] 40 | ElTable: typeof import('element-plus/es')['ElTable'] 41 | ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] 42 | ElTabPane: typeof import('element-plus/es')['ElTabPane'] 43 | ElTabs: typeof import('element-plus/es')['ElTabs'] 44 | ElTag: typeof import('element-plus/es')['ElTag'] 45 | ElTooltip: typeof import('element-plus/es')['ElTooltip'] 46 | ElUpload: typeof import('element-plus/es')['ElUpload'] 47 | HomeFooter: typeof import('./src/components/HomeFooter/index.vue')['default'] 48 | HomeHeader: typeof import('./src/components/HomeHeader/index.vue')['default'] 49 | InfosForm: typeof import('./src/components/InfosForm/index.vue')['default'] 50 | LinkDialog: typeof import('./src/components/linkDialog.vue')['default'] 51 | LoginPanel: typeof import('./src/components/loginPanel.vue')['default'] 52 | MessageList: typeof import('./src/components/MessageList/index.vue')['default'] 53 | MessagePanel: typeof import('./src/components/MessagePanel/index.vue')['default'] 54 | Praise: typeof import('./src/components/Praise/index.vue')['default'] 55 | QrCode: typeof import('./src/components/QrCode.vue')['default'] 56 | RouterLink: typeof import('vue-router')['RouterLink'] 57 | RouterView: typeof import('vue-router')['RouterView'] 58 | } 59 | export interface ComponentCustomProperties { 60 | vLoading: typeof import('element-plus/es')['ElLoadingDirective'] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM sugarjl/debian:latest 2 | ENV PNPM_HOME="/root/.local/share/pnpm" 3 | ENV PATH="$PNPM_HOME:$PATH" 4 | 5 | # 拷贝ep资源 6 | COPY ./client /root/client 7 | COPY ./server /root/server 8 | COPY ./start.sh /start.sh 9 | COPY ./nginx.conf /etc/nginx/sites-enabled/default 10 | COPY ./mysql.cnf /etc/mysql/conf.d 11 | COPY ./auto_create.sql /easypicker2.sql 12 | 13 | # 环境准备 14 | RUN mkdir -p /usr/share/easypicker/ng-logs \ 15 | && mv /root/client /usr/share/easypicker \ 16 | && mv /root/server /usr/share/easypicker \ 17 | && pnpm config set registry https://registry.npmmirror.com/ \ 18 | && cd /usr/share/easypicker/server && pnpm install -P \ 19 | && pnpm add pm2 -g 20 | 21 | EXPOSE 80 22 | 23 | CMD ["bash", "./start.sh"] -------------------------------------------------------------------------------- /docker/base.sh: -------------------------------------------------------------------------------- 1 | # 目录准备 2 | if [ ! -d "./client" ]; then 3 | mkdir ./client 4 | fi 5 | if [ ! -d "./server" ]; then 6 | mkdir ./server 7 | fi 8 | 9 | # 拷贝服务端资源(不在当前项目里,以后优化) 10 | rm -rf ./client/dist 11 | cp -rf ../dist ./client/dist 12 | 13 | # 拷贝服务端资源(不在当前项目里,以后优化) 14 | rm -rf ./server/dist 15 | cp -rf ./../../easypicker2-server/dist ./server 16 | cp -rf ./../../easypicker2-server/package.json ./server 17 | cp -rf ./../../easypicker2-server/pnpm-lock.yaml ./server -------------------------------------------------------------------------------- /docker/build-beta.sh: -------------------------------------------------------------------------------- 1 | bash ./base.sh 2 | docker buildx build -t sugarjl/easypicker:beta --platform=linux/arm64,linux/amd64 . --push -------------------------------------------------------------------------------- /docker/build-debian.sh: -------------------------------------------------------------------------------- 1 | docker buildx build -f debian.Dockerfile -t sugarjl/debian:latest --platform=linux/arm64,linux/amd64 . --push -------------------------------------------------------------------------------- /docker/build-latest.sh: -------------------------------------------------------------------------------- 1 | bash ./base.sh 2 | docker buildx build -t sugarjl/easypicker:latest --platform=linux/arm64,linux/amd64 . --push -------------------------------------------------------------------------------- /docker/build-local.sh: -------------------------------------------------------------------------------- 1 | bash ./base.sh 2 | docker build -t sugarjl/easypicker:local . -------------------------------------------------------------------------------- /docker/build-version.mjs: -------------------------------------------------------------------------------- 1 | import pkg from '../package.json' assert { type: 'json' } 2 | 3 | await $`bash ./base.sh` 4 | await $`docker buildx build -t sugarjl/easypicker:${pkg.version} --platform=linux/arm64,linux/amd64 . --push` 5 | -------------------------------------------------------------------------------- /docker/client/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ATQQ/easypicker2-client/2a352d0999889e052eeb5e2248e3dae093bbb5a6/docker/client/.gitkeep -------------------------------------------------------------------------------- /docker/debian.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | 3 | RUN touch /etc/apt/sources.list \ 4 | && echo "deb http://mirrors.aliyun.com/debian bullseye main" >/etc/apt/sources.list \ 5 | && echo "deb http://mirrors.aliyun.com/debian-security bullseye-security main" >>/etc/apt/sources.list \ 6 | && echo "deb http://mirrors.aliyun.com/debian bullseye-updates main" >>/etc/apt/sources.list \ 7 | && mv /etc/apt/sources.list.d/debian.sources /etc/apt 8 | 9 | # 安装curl 10 | RUN apt update && apt install -y curl 11 | # 安装pnpm 12 | RUN curl -fsSL https://get.pnpm.io/install.sh | bash - 13 | 14 | ENV PNPM_HOME="/root/.local/share/pnpm" 15 | ENV PATH="$PNPM_HOME:$PATH" 16 | 17 | # 安装Node 18 | RUN pnpm env use --global lts 19 | 20 | # 安装nginx 21 | RUN apt install -y nginx 22 | 23 | # 安装redis 24 | RUN apt install -y redis-server 25 | 26 | COPY ./sources.list /etc/apt/sources.list 27 | 28 | # 安装mysql 29 | RUN apt update && apt install -y default-mysql-server default-mysql-client 30 | 31 | # 安装mongodb 32 | RUN echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-6.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-6.0.list \ 33 | && apt-get -y install gnupg \ 34 | && curl -fsSL https://pgp.mongodb.com/server-6.0.asc | gpg -o /usr/share/keyrings/mongodb-server-6.0.gpg --dearmor \ 35 | && apt update && apt install -y mongodb-org \ 36 | && mkdir -p /var/lib/mongo -------------------------------------------------------------------------------- /docker/mysql.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | user = root 3 | pid-file = /var/run/mysqld/mysqld.pid 4 | socket = /var/run/mysqld/mysqld.sock 5 | port = 3306 6 | datadir = /var/lib/mysql 7 | 8 | [Service] 9 | User=mysql 10 | Group=mysql -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | listen [::]:80 default_server; 4 | 5 | server_name _; 6 | index index.html; 7 | root /usr/share/easypicker/client/dist; 8 | 9 | # vue-router 10 | location / { 11 | try_files $uri $uri/ /index.html; 12 | } 13 | 14 | #PROXY-START/api 15 | 16 | location ^~ /api/ { 17 | proxy_pass http://127.0.0.1:3000/; 18 | proxy_set_header Host 127.0.0.1; 19 | proxy_set_header X-Real-IP $remote_addr; 20 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 21 | proxy_set_header REMOTE-HOST $remote_addr; 22 | 23 | add_header X-Cache $upstream_cache_status; 24 | 25 | #Set Nginx Cache 26 | sub_filter "/api" ""; 27 | sub_filter_once off; 28 | 29 | 30 | set $static_file6DkW7ygY 0; 31 | if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) { 32 | set $static_file6DkW7ygY 1; 33 | expires 12h; 34 | } 35 | if ( $static_file6DkW7ygY = 0 ) { 36 | add_header Cache-Control no-cache; 37 | } 38 | } 39 | #PROXY-END/api 40 | 41 | location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) { 42 | return 404; 43 | } 44 | 45 | if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) { 46 | return 403; 47 | } 48 | 49 | access_log /usr/share/easypicker/ng-logs/ep.log; 50 | error_log /usr/share/easypicker/ng-logs/ep.error.log; 51 | } -------------------------------------------------------------------------------- /docker/server/.env: -------------------------------------------------------------------------------- 1 | MYSQL_DB_HOST=127.0.0.1 2 | MYSQL_DB_PORT=3306 3 | MYSQL_DB_NAME=easypicker2 4 | MYSQL_DB_USER=root 5 | MYSQL_DB_PWD=easypicker2 6 | 7 | MONGO_DB_HOST=127.0.0.1 8 | MONGO_DB_PORT=27017 9 | MONGO_DB_NAME=easypicker2 10 | MONGO_DB_USER=easypicker2 11 | MONGO_DB_PWD=easypicker2 12 | MONGO_DB_NEED_AUTH=false 13 | 14 | REDIS_DB_HOST=127.0.0.1 15 | REDIS_DB_PORT=6379 16 | REDIS_DB_PASSWORD=easypicker2 17 | REDIS_DB_NEED_AUTH=false 18 | 19 | SERVER_PORT=3000 20 | SERVER_HOST=localhost 21 | 22 | QINIU_ACCESS_KEY=AccessKey 23 | QINIU_SECRET_KEY=SecretKey 24 | QINIU_BUCKET_NAME=BucketName 25 | QINIU_BUCKET_DOMAIN=BucketDomain 26 | QINIU_BUCKET_IMAGE_COVER_STYLE=false 27 | QINIU_BUCKET_IMAGE_PREVIEW_STYLE=false 28 | QINIU_BUCKET_ZONE=huanan 29 | 30 | TENCENT_SECRET_ID=test 31 | TENCENT_SECRET_KEY=test 32 | TENCENT_MESSAGE_TemplateID=12345 33 | TENCENT_MESSAGE_SmsSdkAppid=12345 34 | TENCENT_MESSAGE_SignName=粥里有勺糖 -------------------------------------------------------------------------------- /docker/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ep-server", 3 | "version": "2.6.1-beta.1", 4 | "private": true, 5 | "description": "", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "dev": "cross-env NODE_ENV=development FW_LOGGING=true run-p build:watch dev:server", 9 | "dev:server": "nodemon dist/index.js --ignore 'upload/*' --ignore user-config.json", 10 | "build:watch": "tsup --watch", 11 | "start:ts": "esno ./src/index.ts", 12 | "start": "cross-env NODE_ENV=production node ./dist/index.js", 13 | "lint": "eslint --fix ./src --ext .ts,.d.ts,.js", 14 | "test": "vitest", 15 | "deploy": "zx scripts/deploy/env-prod.mjs", 16 | "deploy:test": "zx scripts/deploy/env-test.mjs", 17 | "build": "tsup", 18 | "upload:oss": "pnpm build && q ep server -up" 19 | }, 20 | "keywords": [], 21 | "author": "ATQQ", 22 | "license": "ISC", 23 | "dependencies": { 24 | "@swc/core": "^1.3.68", 25 | "cross-env": "^7.0.3", 26 | "dayjs": "^1.11.7", 27 | "flash-wolves": "^0.4.1", 28 | "formidable": "^2.0.1", 29 | "mongodb": "^3.7.3", 30 | "mysql": "^2.18.1", 31 | "qiniu": "^7.4.0", 32 | "redis": "^3.1.2", 33 | "reflect-metadata": "^0.1.13", 34 | "tencentcloud-sdk-nodejs": "^4.0.318", 35 | "typeorm": "^0.3.17" 36 | }, 37 | "devDependencies": { 38 | "@types/mongodb": "^3.6.20", 39 | "@types/mysql": "^2.15.21", 40 | "@types/node": "^14.18.11", 41 | "@types/redis": "^2.8.32", 42 | "@typescript-eslint/eslint-plugin": "^5.12.0", 43 | "@typescript-eslint/parser": "^5.12.0", 44 | "eslint": "^8.9.0", 45 | "eslint-config-airbnb-base": "^15.0.0", 46 | "eslint-config-prettier": "^8.5.0", 47 | "eslint-plugin-import": "^2.25.4", 48 | "eslint-plugin-prettier": "^4.2.1", 49 | "eslint-plugin-todo-ddl": "^1.1.1", 50 | "esno": "^0.14.1", 51 | "nodemon": "^2.0.15", 52 | "npm-run-all": "^4.1.5", 53 | "prettier": "^2.7.1", 54 | "ts-node": "^10.9.1", 55 | "tsup": "^7.1.0", 56 | "typescript": "^4.5.5", 57 | "vitest": "^0.9.2", 58 | "zx": "^5.1.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /docker/sources.list: -------------------------------------------------------------------------------- 1 | deb https://mirrors.aliyun.com/debian/ bookworm main non-free non-free-firmware contrib 2 | deb-src https://mirrors.aliyun.com/debian/ bookworm main non-free non-free-firmware contrib 3 | deb https://mirrors.aliyun.com/debian-security/ bookworm-security main 4 | deb-src https://mirrors.aliyun.com/debian-security/ bookworm-security main 5 | deb https://mirrors.aliyun.com/debian/ bookworm-updates main non-free non-free-firmware contrib 6 | deb-src https://mirrors.aliyun.com/debian/ bookworm-updates main non-free non-free-firmware contrib 7 | deb https://mirrors.aliyun.com/debian/ bookworm-backports main non-free non-free-firmware contrib 8 | deb-src https://mirrors.aliyun.com/debian/ bookworm-backports main non-free non-free-firmware contrib -------------------------------------------------------------------------------- /docker/start.sh: -------------------------------------------------------------------------------- 1 | # 启动 mysql 2 | chown -R root /var/lib/mysql 3 | mysqld_safe & 4 | 5 | sleep 3 6 | # 导入表信息 7 | mysqladmin -u root password "easypicker2" 8 | mysql -uroot -peasypicker2 -e "CREATE DATABASE IF NOT EXISTS easypicker2;" 9 | mysql -uroot -peasypicker2 -e "show databases;use easypicker2;source /easypicker2.sql;show tables;" 10 | 11 | # 启动 nginx 12 | nginx -c /etc/nginx/nginx.conf 13 | 14 | # 启动 redis 15 | redis-server /etc/redis/redis.conf 16 | 17 | # 启动 mongodb 18 | mongod --dbpath /var/lib/mongo --logpath /var/log/mongodb/mongod.log --fork 19 | 20 | # 启动 pm2 21 | cd /usr/share/easypicker/server && pm2-runtime start pnpm --name ep-server -- run start -------------------------------------------------------------------------------- /docs/.env: -------------------------------------------------------------------------------- 1 | VITE_APP_AXIOS_BASE_URL=/api/ -------------------------------------------------------------------------------- /docs/.env.production: -------------------------------------------------------------------------------- 1 | # .env.production 2 | VITE_APP_AXIOS_BASE_URL=https://ep.sugarat.top/api/ -------------------------------------------------------------------------------- /docs/.vitepress/theme/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ATQQ/easypicker2-client/2a352d0999889e052eeb5e2248e3dae093bbb5a6/docs/.vitepress/theme/bg.png -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.scss: -------------------------------------------------------------------------------- 1 | .VPHome { 2 | .VPHero { 3 | --vp-home-hero-image-background-image: linear-gradient( -45deg, #a0cfff73 30%, #35495e80 ); 4 | --vp-home-hero-image-filter: blur(100px); 5 | .container { 6 | .main { 7 | span.clip { 8 | background: -webkit-linear-gradient( 9 | 315deg, 10 | #42d392 25%, 11 | #647eff 12 | ); 13 | background-clip: text; 14 | -webkit-background-clip: text; 15 | -webkit-text-fill-color: transparent; 16 | } 17 | .VPButton.docs { 18 | border-color: rgb( 19 | 160, 20 | 207, 21 | 255 22 | ); 23 | color: rgb(64, 158, 255); 24 | background-color: rgb( 25 | 236, 26 | 245, 27 | 255 28 | ); 29 | } 30 | .VPButton.docs:hover { 31 | color: rgb(236, 245, 255); 32 | background-color: rgb( 33 | 64, 34 | 158, 35 | 255 36 | ); 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import './index.scss' 2 | 3 | import BlogTheme from '@sugarat/theme' 4 | 5 | export default BlogTheme 6 | -------------------------------------------------------------------------------- /docs/author.md: -------------------------------------------------------------------------------- 1 | # 关于我 2 | 3 | 99年出生,标准的理工男一枚,毕业于 **[西南石油大学](https://www.swpu.edu.cn/)** ,热爱开源与知识分享 4 | 5 | 目前就职于 🛵美团🛵(Base 北京到店餐饮),有兴趣成为同事的话,可以小窗私我了解岗位详情 6 | 7 | ![](https://img.cdn.sugarat.top/mdImg/MTYwNDcyMTQ4NTMyOA==604721485328) 8 | 9 | ## 联系作者 10 | 11 | 12 | ## 相关站点 13 | * [个人博客](https://sugarat.top) 14 | * [掘金](https://juejin.cn/user/1028798615918983/posts) 15 | * [GitHub](https://github.com/ATQQ) 16 | * [博客园](https://www.cnblogs.com/roseAT/) 17 | 18 | 19 | ## 其它作品 20 | * [个人图床](https://imgbed.sugarat.top) 21 | * [考勤小程序](https://hdkq.sugarat.top/) 22 | * [时光恋人](https://lover.sugarat.top) 23 | * [在线简历生成](https://resume.sugarat.top/) 24 | * 。。。 -------------------------------------------------------------------------------- /docs/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | // We suggest you to commit this file into source control 3 | declare global { 4 | 5 | } 6 | export {} 7 | -------------------------------------------------------------------------------- /docs/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | Avatar: typeof import('./src/components/Avatar.vue')['default'] 11 | Callme: typeof import('./src/components/callme/index.vue')['default'] 12 | Home: typeof import('./src/components/Home.vue')['default'] 13 | Picture: typeof import('./src/components/Picture.vue')['default'] 14 | Praise: typeof import('./src/components/Praise.vue')['default'] 15 | RouterLink: typeof import('vue-router')['RouterLink'] 16 | RouterView: typeof import('vue-router')['RouterView'] 17 | WishBtn: typeof import('./src/components/WishBtn.vue')['default'] 18 | WishPanel: typeof import('./src/components/WishPanel.vue')['default'] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/deploy/design/api.md: -------------------------------------------------------------------------------- 1 | # 接口设计 2 | 3 | 未完待续 4 | -------------------------------------------------------------------------------- /docs/deploy/design/db.md: -------------------------------------------------------------------------------- 1 | # 数据库设计 2 | 3 | 未完待续 4 | -------------------------------------------------------------------------------- /docs/deploy/design/index.md: -------------------------------------------------------------------------------- 1 | # 应用设计 2 | 3 | 未完待续 -------------------------------------------------------------------------------- /docs/deploy/design/shell.md: -------------------------------------------------------------------------------- 1 | # 自动部署脚本 2 | 3 | ## Shell脚本源码 4 | * [GitHub](https://github.com/ATQQ/easypicker2-server/tree/master/scripts/ep) 5 | * [Gitee](https://gitee.com/sugarjl/easypicker2-server/tree/master/scripts/ep) 6 | 7 | ## CLI工具源码 8 | * [Github: @sugarat/cli](https://github.com/ATQQ/tools/tree/main/packages/cli/dynamic-cli/core) 9 | * [Github: @sugarat/cli-plugin-ep](https://github.com/ATQQ/tools/tree/main/packages/cli/dynamic-cli/plugins/cli-plugin-ep) -------------------------------------------------------------------------------- /docs/deploy/docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2,3] 3 | --- 4 | 5 | # 使用docker部署 6 | 7 | :::tip 关于镜像的一点说明❤️ 8 | 基于 [debian](https://hub.docker.com/_/debian) 构建,默认安装了 Nginx,Redis,PNPM,Node,MySQL,MongoDB(打包为 [sugarjl/debian](https://hub.docker.com/repository/docker/sugarjl/debian/general) - [debian.Dockerfile](https://github.com/ATQQ/easypicker2-client/blob/main/docker/debian.Dockerfile)) 9 | 10 | easypicker 镜像相关资源 11 | 12 | - 镜像地址:[sugarjl/easypicker](https://hub.docker.com/repository/docker/sugarjl/easypicker/general) 13 | - 构建脚本:[docker/build-latest.sh](https://github.com/ATQQ/easypicker2-client/blob/main/docker/build-latest.sh),[docker/build-beta.sh](https://github.com/ATQQ/easypicker2-client/blob/main/docker/build-beta.sh),[docker/build-version.mjs](https://github.com/ATQQ/easypicker2-client/blob/main/docker/build-version.mjs) 14 | - Dockerfile:[docker/Dockerfile](https://github.com/ATQQ/easypicker2-client/blob/main/docker/Dockerfile) 15 | 16 | 如果你希望是用宿主机的数据库,请阅最后一部分自定义镜像 17 | 18 | **如果你对镜像构建改进有更好的建议,欢迎私聊或提issue讨论。** 19 | 20 | _笔者使用的加速镜像源为:`https://dockerproxy.com`_ 21 | ::: 22 | 23 | ## 快速开始 24 | 25 | ① 获取镜像 26 | 27 | ```sh 28 | docker pull sugarjl/easypicker 29 | ``` 30 | 31 | ② 启动镜像,并设置一个映射的端口 32 | 33 | 这里设置为`6478`,同时设置容器名为`easypicker2`(这些都可以根据实际情况进行修改) 34 | 35 | ```sh 36 | docker run -d -p 6478:80 --name easypicker2 sugarjl/easypicker 37 | ``` 38 | 39 | 运行`docker ps` 如果看到下面类似的日志,_恭喜你,你已经成功启动了 easypicker2 🎉_ 40 | 41 | ![](https://img.cdn.sugarat.top/mdImg/MTY5Nzk2OTc3MDM4MA==697969770380) 42 | 43 | 可以打开浏览器访问一下 http://localhost:6478 即可看到页面 44 | 45 | ③ 获取系统账号,登录后台 46 | 47 | ```sh 48 | docker logs easypicker2 49 | ``` 50 | 51 | ![](https://img.cdn.sugarat.top/mdImg/MTY5Nzk3MTc4MzQ1MA==697971783450) 52 | 53 | 访问 http://localhost:6478/login ,输入账号密码即可登录后台 54 | 55 | ![](https://img.cdn.sugarat.top/mdImg/MTY3Njc5OTQwNTY2Nw==676799405667) 56 | 57 | ## 配置服务 58 | 59 | 根据实际的情况完成 七牛云,腾讯云(可选)的配置 60 | 61 | ### 七牛云 62 | 63 | 参考[七牛云OSS服务创建](./qiniu.md)文章,获取七牛云相关的几个环境变量 64 | 65 | ### 设置管理员 66 | 67 | 参考[线上部署文档](./online-new.md#%E9%85%8D%E7%BD%AE%E7%AE%A1%E7%90%86%E5%91%98%E6%9D%83%E9%99%90)最后一部分内容,设置管理员 68 | 69 | _user 表里,将对应的用户 `power` 值设置为 0 即可_ 70 | 71 | 下面是操作案例,将 `admin` 账号设置为管理员 72 | 73 | _执行sql修改目标账号权限,修改最后的 **account='admin'** 为目标账号即可_ 74 | 75 | ```sh 76 | docker exec easypicker2 mysql -uroot -peasypicker2 -e "use easypicker2;UPDATE user SET power=0 WHERE account='admin';" 77 | ``` 78 | 79 | 重新登录账号即可生效 80 | 81 | ## 更新容器 82 | 83 | ① 备份数据 84 | 85 | ```sh 86 | # 创建备份目录 87 | mkdir ep_backup 88 | 89 | # 备份服务配置文件 90 | docker cp easypicker2:/usr/share/easypicker/server/user-config.json ep_backup/user-config.json 91 | 92 | # 备份mysql 93 | docker exec easypicker2 mysqldump -uroot -peasypicker2 easypicker2 > ep_backup/easypicker2.sql 94 | 95 | # 备份mongodb 96 | docker exec easypicker2 mongodump -d easypicker2 -o /tmp/ep_backup 97 | docker cp easypicker2:/tmp/ep_backup ep_backup/mongodb 98 | ``` 99 | 100 | ② 停止容器 101 | 102 | ```sh 103 | docker stop easypicker2 104 | ``` 105 | 106 | ③ 更新镜像 107 | 108 | ```sh 109 | docker pull sugarjl/easypicker 110 | ``` 111 | 112 | ④ 重新创建容器 113 | :::warning 注意事项 114 | 如果要继续使用之前的容器名,需要先删除之前的容器 115 | 116 | 请确保相关数据都有备份,删除容器后无法恢复 117 | 118 | 否则使用一个新的镜像名(例如 easypicker-next) 119 | 120 | ```sh 121 | # 移除旧容器 122 | docker rm easypicker2 123 | ``` 124 | 125 | ::: 126 | 127 | ```sh 128 | # 重新创建新的容器 129 | docker run -d -p 6478:80 --name easypicker2 sugarjl/easypicker 130 | ``` 131 | 132 | ⑤ 恢复数据 133 | 134 | ```sh 135 | # 恢复配置 136 | docker cp ep_backup/user-config.json easypicker2:/usr/share/easypicker/server/user-config.json 137 | # 恢复mysql 138 | docker exec -i easypicker2 mysql -uroot -peasypicker2 easypicker2 < ep_backup/easypicker2.sql 139 | # 恢复mongodb 140 | docker cp ep_backup/mongodb easypicker2:/tmp/ep_backup 141 | docker exec easypicker2 mongorestore -d easypicker2 /tmp/ep_backup/easypicker2 142 | ``` 143 | 144 | ⑥ 重启容器 145 | 146 | ```sh 147 | docker restart easypicker2 148 | ``` 149 | 150 | ## 🚧 自定义镜像 151 | 152 | ## FAQ 153 | 154 | ### Q1: 启动后,容器自动关闭 155 | 156 | 查看日志 157 | 158 | ```sh 159 | docker logs easypicker2 160 | ``` 161 | 162 | 如果有如下报错信息 163 | 164 | ```sh 165 | SyntaxError: Unexpected token { in JSON at position 2765 166 | at JSON.parse () 167 | at /usr/share/easypicker/server/dist/index.js:622:48 168 | at step (/usr/share/easypicker/server/dist/index.js:337:23) 169 | at Object.next (/usr/share/easypicker/server/dist/index.js:278:20) 170 | at asyncGeneratorStep (/usr/share/easypicker/server/dist/index.js:20:28) 171 | at _next (/usr/share/easypicker/server/dist/index.js:38:17) 172 | ``` 173 | 174 | 说明配置文件 `user-config.json` 格式有误,可以CV出来修改一下 175 | 176 | ```sh 177 | # 复制到当前目录下下 178 | docker cp easypicker2:/usr/share/easypicker/server/user-config.json user-config.json 179 | ``` 180 | 181 | 使用[JSON编辑器打开](https://www.json.cn/),查看错误的位置 182 | 183 | 例如这里的错误 184 | 185 | ![](https://img.cdn.sugarat.top/mdImg/MTY5Nzk3NjM5NjM1NA==697976396354) 186 | 187 | 修复正确后,将配置文件重新复制到容器内即可 188 | 189 | ```sh 190 | # 配置文件复制到容器内 191 | docker cp user-config.json easypicker2:/usr/share/easypicker/server/user-config.json 192 | # 重新启动 193 | docker restart easypicker2 194 | ``` 195 | 196 | ### Q2: 宝塔面板如何使用docker部署 197 | 198 | 1 宝塔面板安装docker 199 | 200 | 2 Docker 管理器中配置加速器 201 | 202 | ```json 203 | { 204 | "registry-mirrors": [ 205 | "https://dockerproxy.com" 206 | ] 207 | } 208 | ``` 209 | 210 | 3 按照上面的步骤运行镜像 211 | 212 | 镜像启动后可以通过 `docker ps` 查看容器运行情况 213 | 214 | ![](https://img.cdn.sugarat.top/mdImg/MTY5OTU0MDA2NDIxMA==699540064210) 215 | 216 | 4 创建网站 217 | 218 | 在网站的 nginx 配置文件中添加`/`路由,通过反向代理将流量转发至 docker 启动的容器 219 | 220 | ![](https://img.cdn.sugarat.top/mdImg/MTY5OTU0MDc2MTMzMg==699540761332) 221 | 222 | ```sh 223 | location / { 224 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 225 | proxy_set_header X-Real-IP $remote_addr; 226 | proxy_pass http://172.17.0.1:6480; 227 | } 228 | ``` 229 | 230 | 现在访问网站就可以看到内容了 231 | -------------------------------------------------------------------------------- /docs/deploy/faq.md: -------------------------------------------------------------------------------- 1 | # 常见问题 2 | 协助自助排查`部署问题` 3 | 4 | ## Q1:PM2/后端服务启动失败,如何手动启动后端服务 5 | 查看 `pm2` 应用列表 6 | ```shell 7 | pm2 ls 8 | ``` 9 | ![](https://img.cdn.sugarat.top/mdImg/MTY1NTM0NTI1MDEzOQ==655345250139) 10 | 11 | 观察服务重启次数是否一直在增加 12 | 13 | 查看服务日志 14 | 15 | ```sh 16 | # 所有日志 17 | pm2 log 18 | # 指定应用日志,如ep-dev 19 | pm2 log ep-dev 20 | ``` 21 | 22 | 如有报错,将报错信息贴至交流群,协助开发者排查 23 | 24 | 如果pm2启动失败,尝试更新 `pm2` 25 | 26 | ```sh 27 | npm i -g pm2 28 | ``` 29 | 30 | 完成升级后,手动启动服务 31 | 1. 先确保当前执行目录在服务目录下 32 | 33 | ![](https://img.cdn.sugarat.top/mdImg/MTY1NTM0NTYzMzk0Nw==655345633947) 34 | 35 | 2. 删除旧的服务 36 | 37 | ```sh 38 | # 查看服务列表 39 | pm2 ls 40 | # 删除指定服务,如ep-dev 41 | pm2 del ep-dev 42 | ``` 43 | 44 | 3. 启动服务 45 | 46 | ```sh 47 | pm2 start npm --name 自定义服务名 -- run start 48 | # 例如 49 | pm2 start npm --name my-ep2-server -- run start 50 | ``` 51 | 52 | ![](https://img.cdn.sugarat.top/mdImg/MTY1NTM0NTg4MTQzNw==655345881437) 53 | 54 | 4. 查看服务情况 55 | 56 | ```sh 57 | pm2 log my-ep2-server 58 | ``` 59 | 60 | :::warning 如有报错红色的信息 61 | 执行指令停止服务 62 | ```sh 63 | pm2 stop my-ep2-server 64 | ``` 65 | 66 | ![](https://img.cdn.sugarat.top/mdImg/MTY1NTM0NjEwODI3Nw==655346108277) 67 | ::: 68 | 69 | 没有错误就完事大吉 70 | 71 | ## Q2:批量下载出错,能上传 72 | 73 | 这种情况一般是七牛云的存储空间区域没有配置对 74 | 75 | 根据服务版本。 76 | 77 | ### > v2.1.7 78 | 见最新 [接入七牛云OSS服务](./qiniu.md) 文档,根据自己的区域配置 `存储空间区域` 值。 79 | 80 | ### <= v2.1.7 81 | 82 | 修改代码`src/utils/qiniuUtil.ts`第`251`行,Zone的值为对应区域的值 83 | 84 | ![](https://img.cdn.sugarat.top/mdImg/MTY1NTM0Njg4NDIxNQ==655346884215) 85 | 86 | ![](https://img.cdn.sugarat.top/mdImg/MTY1NTM0Njk0NTY2Mw==655346945663) 87 | -------------------------------------------------------------------------------- /docs/deploy/index.md: -------------------------------------------------------------------------------- 1 | # 私有化部署 2 | 3 | 此部分将介绍本地启动&线上的部署方法 4 | 5 | ## 目录 6 | * [本地启动](./local.md) 7 | * [使用docker部署](./docker.md) 8 | * ~~[线上部署 - 宝塔面板v1](./online.md)~~ 9 | * ~~[线上部署 - 宝塔面板v2](./online-new.md)~~ 10 | * [线上部署 - v3](./online-v3.md) 11 | * [七牛云OSS服务创建 - 七牛云相关配置](./qiniu.md) 12 | * [FAQ&常见问题](./faq.md) -------------------------------------------------------------------------------- /docs/deploy/qiniu.md: -------------------------------------------------------------------------------- 1 | # 七牛云OSS配置 2 | 3 | 文件存储采用七牛云的[OSS](https://www.qiniu.com/products/kodo)(对象存储服务) 4 | 5 | 这部分将手把手介绍如何在本项目中接入七牛云OSS 6 | 7 | :::tip 为什么使用七牛云? 8 | 9 | - 因为资费便宜,还有**30G**的免费额度 10 | ::: 11 | 12 | ## 1. 账号注册 13 | 14 | 访问[七牛云-注册页面](https://portal.qiniu.com/signup?redirect_url=https:~2F~2Fwww.qiniu.com~2Fproducts~2Fkodo) 注册一个账号 15 | 16 | ## 2. 创建存储空间 17 | 18 | 访问[七牛云-对象存储](https://www.qiniu.com/products/kodo) 19 | 20 | 戳页面上的立即使用 21 | 22 | ![](https://img.cdn.sugarat.top/mdImg/MTY0NzU2OTQ5MzAyNg==647569493026) 23 | 24 | 新建空间,输入一些必要的数据 25 | 26 | ![](https://cdn.upyun.sugarat.top/mdImg/sugar/20c5b44a7a673c6ce0c5aef57e436328) 27 | 28 | 其中**访问控制**一定记得选私有,避免文件不通过鉴权就被下载 29 | 30 | :::tip 31 | 32 | - 存储空间名即为,后端服务中`.env`中`QINIU_BUCKET_NAME`的值 33 | - 存储区域对应后端服务`.env`中`QINIU_BUCKET_ZONE`的值 34 | ::: 35 | 36 | `QINIU_BUCKET_ZONE`可选值如下 37 | | 存储区域 | 值 | 38 | | -------- | ------------- | 39 | | 华东 | huadong | 40 | | 华北 | huabei | 41 | | 华南 | huanan | 42 | | 北美 | beimei | 43 | | 东南亚 | SoutheastAsia | 44 | 45 | 创建成功提示,测试域名有**30天**有效期 46 | 47 | ![](https://img.cdn.sugarat.top/mdImg/MTY0NzU2OTc1ODczNA==647569758734) 48 | 49 | 如果需要长期使用,建议绑定一个自定义域名, 50 | 51 | :::tip 我没有域名怎么办 52 | 当然如果你没有域名,可以[联系作者](../author.md),提供一个`.sugarat.top`下的3级,4级域名 53 | ::: 54 | 55 | ## 3. 获取到域名 56 | 57 | 进入我们创建的空间`easypicker-test`,就能看到提供的测试域名 58 | 59 | ![](https://img.cdn.sugarat.top/mdImg/MTY0NzU2OTk3NjcwMQ==647569976702) 60 | 61 | 如果需要添加自己的域名,请在下面的页面自定义源站域名绑定自己的域名映射到空间内 62 | 63 | ![](https://cdn.upyun.sugarat.top/mdImg/sugar/3888bbaeb43e8c4dcd23cf24824fef52) 64 | 65 | ## 4. 获取ack与sek 66 | 67 | :::warning 重要提示!!!- 68 | **这两个东西千万不要泄露!!!** 69 | 70 | **这两个东西千万不要泄露!!!** 71 | ::: 72 | 73 | 当然泄漏了可自己进行重置 74 | 75 | 获取位置如下 76 | 77 | 控制面板右上角的秘钥管理 78 | 79 | ![](https://img.cdn.sugarat.top/mdImg/MTY0NzU3MDI3MDQwMw==647570270403) 80 | 81 | 接下来就能看到 82 | 83 | ![](https://img.cdn.sugarat.top/mdImg/MTY0NzU3MDM1MTUxOA==647570351518) 84 | 85 | :::tip 86 | 87 | - `AK` 对应`.env`中的 `QINIU_ACCESS_KEY` 88 | - `SC` 对应`.env`中的 `QINIU_SECRET_KEY` 89 | ::: 90 | 91 | ## 5. 通过面板快速更新配置 92 | 93 | 到此七牛云相关的 **5** 个必要需要的环境变量我们都拿到了 94 | 95 | - QINIU_BUCKET_ZONE: 存储区域 96 | - QINIU_BUCKET_NAME: 存储空间名 97 | - QINIU_BUCKET_DOMAIN: 绑定的域名 98 | - QINIU_ACCESS_KEY: 访问密钥 99 | - QINIU_SECRET_KEY: 安全密钥 100 | 101 | 将其更新到管理面板中七牛云配置的位置即可 102 | 103 | ![](https://img.cdn.sugarat.top/mdImg/MTY1OTkzNjMzMTE2Mg==659936331162) 104 | 105 | :::danger 注意:域名的值需要加上协议 106 | 107 | - 注意:这里的值需要加上协议`http://你的域名` 108 | - 注意:这里的值需要加上协议`http://你的域名` 109 | - 注意:这里的值需要加上协议`http://你的域名` 110 | - **如果升级了https,这里对应填入https** 111 | 112 | ::: 113 | 114 | :::details 如果应用版本 < v2.1.9,需要手动更新 115 | 手动将上述配置内容填写到,后端服务中`.env`中对应位置,然后重启服务即可 116 | ::: 117 | 118 | ## 6. 添加必要响应头信息 119 | 120 | 目的:避免图片,pdf,txt等浏览器支持预览的文件直接被预览而不触发下载 121 | 122 | 首先找到对应的存储空间,选择绑定的域名查看详情 123 | 124 | ![](https://img.cdn.sugarat.top/mdImg/MTY1OTkzNjgxOTc4OA==659936819788) 125 | 126 | 在打开的详情页面中找到 `HTTP响应头配置` 127 | 128 | ![](https://img.cdn.sugarat.top/mdImg/MTY1OTkzNjkwODY2Mw==659936908663) 129 | 130 | 添加一条规则,然后点击确定即可 131 | 132 | ```sh 133 | Content-Disposition attachment 134 | ``` 135 | 136 | ![](https://img.cdn.sugarat.top/mdImg/MTY1OTkzNjk3ODQxMg==659936978412) 137 | 138 | ## 7. 设置图片样式(可选) 139 | 140 | 现在手机拍摄的图片往往都很大,动辄10几兆,为了加快图片的预览与节省服务带宽可以配置七牛云的图片样式进行裁剪 141 | 142 | ![](https://img.cdn.sugarat.top/mdImg/MTY0OTkwMTE5NDY5Mw==649901194693) 143 | 144 | 点击新建图片样式,然后根据指引操作,完成创建 145 | 146 | 共需要两个样式,一个缩略图一个预览图,下面是场景示例 147 | 148 | | 缩略图 | 预览图 | 149 | | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | 150 | | ![](https://img.cdn.sugarat.top/mdImg/MTY0OTkwMTMyOTI3Ng==649901329276) | ![](https://img.cdn.sugarat.top/mdImg/MTY0OTkwMTM0ODcwOA==649901348708) | 151 | 152 | 设置样式分隔符 153 | 154 | ![](https://img.cdn.sugarat.top/mdImg/MTY0OTkwMTc1MzA1OQ==649901753059) 155 | 156 | 在配置面板中更新即可 157 | 158 | - **注意** 159 | - 不同存储空间之间的样式不互通 160 | - 填入格式是分隔符+样式名 161 | 162 | 完成配置后重启服务即可 163 | :::details 如果应用版本 < v2.1.9,需要手动在配置文件中更新 164 | 165 | 将创建好的样式名和分隔符,填入到服务端的环境变量中 166 | 167 | ![](https://img.cdn.sugarat.top/mdImg/MTY0OTkwMTgwOTI3NQ==649901809275) 168 | ::: 169 | 170 | ## 8. 绑定自定义域名(可选) 171 | 172 | 在存储空间里找到`域名管理`,点击绑定域名即可 173 | 174 | ![](https://img.cdn.sugarat.top/mdImg/MTY0NzY5NDUwNTkzNw==647694505937) 175 | 176 | 域名输入一个自己域名对应的2/3/4级域名均可 177 | 178 | - 例如:`sugarat.top` 179 | - 3级域名: `ep.sugarat.top` 180 | - 4级域名: `ep.test.sugarat.top` 181 | - 5级: `ep.test.file.sugarat.top` 182 | 183 | ![](https://img.cdn.sugarat.top/mdImg/MTY0Nzc1MjY5ODk5NA==647752698994) 184 | 185 | 填写完成后点击`创建`即可,然后按照要求添加域名解析 186 | 187 | 可以自行阅读[七牛云提供的域名绑定文档](https://developer.qiniu.com/kodo/8527/kodo-domain-name-management)完成 188 | 189 | 有其它问题可以小群交流,方便可以加入及时交流沟通问题: 685446473 190 | 191 | ![](https://img.cdn.sugarat.top/mdImg/MTY0Nzc1MjI3MzUwMw==647752273503) 192 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: EasyPicker(轻取) 6 | text: 在线文件收取平台 7 | tagline: 一个功能丰富,开源&免费,活跃的Web应用 8 | image: 9 | src: https://img.cdn.sugarat.top/mdImg/MTY3ODAwMzU3MTc2Ng==678003571766 10 | alt: 轻取 11 | actions: 12 | - theme: brand 13 | text: 立即体验 14 | link: https://ep2.sugarat.top 15 | - theme: docs 16 | text: 私有化部署 17 | link: /deploy/ 18 | - theme: docs 19 | text: 应用介绍 20 | link: /introduction/about/index 21 | - theme: alt 22 | text: 提交示例 23 | link: https://ep2.sugarat.top/task/627bd3b18a567f1b47bcdace 24 | - theme: alt 25 | text: 更新日志 26 | link: /plan/log 27 | 28 | features: 29 | - icon: ⚡️ 30 | title: 快速上手 31 | details: 基于Web技术实现,随时随地即可收取,操作简单 32 | - icon: 🖖 33 | title: 安全高效 34 | details: 使用 七牛云OSS 存储所有文件,无空间限制,无上传下载速度限制 35 | - icon: 🛠️ 36 | title: 开源&免费 37 | details: 支持私有化部署 38 | --- 39 | 40 | -------------------------------------------------------------------------------- /docs/introduction/about/code.md: -------------------------------------------------------------------------------- 1 | # 相关源码 2 | 3 | 项目相关的所有代码都是在`GitHub`上开源的,供大家学习与私有化部署 4 | 5 | 有能力者也欢迎参与代码的贡献 6 | 7 | ## 应用链接 8 | 服务一共在线上部署了3套,供大家测试与使用 9 | * 线上环境(绑定了2个域名,对应同一套服务) 10 | * https://ep.sugarat.top/ 11 | * https://ep2.sugarat.top/ 12 | * 测试环境test: 13 | * https://ep.test.sugarat.top/ 14 | * 测试环境dev: 15 | * https://ep.dev.sugarat.top/ 16 | 17 | ## 仓库地址 18 | ### Easypicker2.0 19 | * [Easypicker2.0-客户端](https://github.com/ATQQ/easypicker2-client) 20 | * [Easypicker2.0-服务端](https://github.com/ATQQ/easypicker2-server) 21 | * [客户端所使用的项目模板](https://github.com/ATQQ/vite-vue3-template) 22 | * [服务端使用的项目模板](https://github.com/ATQQ/node-server) 23 | 24 | ### Easypicker1.0(已下线) 25 | * [Easypicker1.0-客户端](https://github.com/ATQQ/EasyPicker-webpack) 26 | * [Easypicker1.0-服务端](https://github.com/ATQQ/easypicker-server) -------------------------------------------------------------------------------- /docs/introduction/about/index.md: -------------------------------------------------------------------------------- 1 | # 应用介绍 2 | 3 | `EasyPicker(轻取)`,一个功能丰富,`开源&免费`的在线文件收取平台 4 | 5 | 自动的对收集的文件进行归档,记录每次提交的文件信息与提交人信息 6 | 7 | 基于`Web`技术实现,不受访问设备限制,随时随地下载,查看收取详细情况 8 | 9 | ## 诞生背景 10 | :::tip 缘于兴趣和机缘巧合 11 | 和大家一样,在学校被收图,收文件反复折磨,当时市面上也没有比较合适好用的成品 12 | 13 | 这给笔者开发 `轻取` 带来了契机,在19年3月28日,码下了第一行代码 14 | 15 | ![](https://img.cdn.sugarat.top/mdImg/MTY1NTU2NDk1NzU3NQ==655564957575) 16 | 17 | ::: 18 | 19 | 校园学习或者工作场景中会有以下几个场景: 20 | 21 | * 电子文件: 班委向同学收取各种实验电子报告 22 | * 图片: 收取各种截图证明/活动照片 23 | * ...等等等 24 | 25 | 目前最广泛的收取方式为,`邮箱`,`QQ`,`微信`等通讯工具传递 26 | 27 | 弊端显而易见,不方便整理统计。还占用电脑/手机内存 28 | 29 | 为了解决这个问题,此项目应运而生 30 | 31 | 当然市面上也有类似的办公工具,只是在功能上有些参差不齐 32 | 33 | ## [现有功能](../feature/index.md) -------------------------------------------------------------------------------- /docs/introduction/feature/admin.md: -------------------------------------------------------------------------------- 1 | # 管理员功能 2 | 3 | :::tip 一点说明 4 | 如何设置账号为管理员权限,请看移步[部署文档](../../deploy/index.md)。 5 | ::: 6 | 7 | ## 全局配置 8 | 9 | ### 路由禁用 10 | 入口:应用管理 > 配置 > 禁用路由管理 11 | 12 | ![](https://cdn.upyun.sugarat.top/mdImg/sugar/fe60f7a8a438e52bb7c56241c30bfd95) 13 | 14 | 禁用对应页面将无法访问,会有如下提示。 15 | 16 | ![](https://cdn.upyun.sugarat.top/mdImg/sugar/c81cd3f31b536bc75fd0d459a08663a7) 17 | 18 | 禁用注册后,会同时隐藏页面上注册相关入口,和禁用注册接口的调用。 -------------------------------------------------------------------------------- /docs/introduction/feature/index.md: -------------------------------------------------------------------------------- 1 | # 一些功能介绍 2 | 3 | TODO: 补全功能介绍 4 | 5 | * [管理员功能](./admin.md) -------------------------------------------------------------------------------- /docs/plan/wish.md: -------------------------------------------------------------------------------- 1 | # 需求墙 2 | > 问题反馈交流新“地盘” => [EasyPicker](https://support.qq.com/product/444158) 3 | 4 | :::danger 优先处理Bug(应用缺陷)!!! 5 | 如有使用上的问题,也在此处反馈 6 | ::: 7 | 8 | :::tip 展示一些用户侧反馈的建议与使用问题 9 | 通过投票决定下一个新功能是什么 10 | 11 | 票数越多优先级越高 12 | 13 | 当然你也可以提出你的建议,让大家来投票 14 | ::: 15 | 16 | :::warning But,优先支持打赏朋友提的功能 17 | 💐 💐 感谢各位[打赏](./../praise/index.md)的朋友,💐 💐 18 | ::: 19 | 20 | 21 | 22 | 23 | 24 | ## 其它方式 25 | 26 | | 加群反馈 | [问卷反馈](https://www.wenjuan.com/s/UZBZJvA040/#《轻取(EasyPicker)用户意见收集》,快来参与吧。【问卷网提供支持】) | 27 | | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | 28 | | QQ群 | 问卷 | 29 | 30 | -------------------------------------------------------------------------------- /docs/praise/index.md: -------------------------------------------------------------------------------- 1 | # 支持作者❤️ 2 | 3 | :::tip 💰 4 | `EasyPicker(轻取)` 是[完全开源](https://github.com/ATQQ/easypicker2-client)&免费的Web应用!!! 5 | 6 | `EasyPicker(轻取)` 是[完全开源](https://github.com/ATQQ/easypicker2-client)&免费的Web应用!!! 7 | 8 | `EasyPicker(轻取)` 是[完全开源](https://github.com/ATQQ/easypicker2-client)&免费的Web应用!!! 9 | ::: 10 | 11 | 缘于兴趣和机缘巧合 12 | 13 | 和大家一样,在学校被`收图`,`收文件`反复折磨,当时市面上也没有比较合适好用的成品 14 | 15 | 这给笔者开发 `轻取` 带来了`契机` 16 | 17 | 目前注册用户已经有了一定规模 18 | 19 | 过程中收到许多网友的称赞与反馈,应用能被大家认可也是非常开心😄 20 | 21 | 如果你觉得应用不错,可以请作者喝茶🍵 22 | 23 | :::warning 应用的开销 24 | 25 | - 存储 `0.099` 元/GB/月 26 | - 下载 `0.43` 元/GB 27 | - CDN-HTTPS-中国大陆`0.28` 元/GB 28 | - CDN 回源流出流量`0.15` 元/GB 29 | - 归档压缩 `0.05` 元/GB 30 | ::: 31 | 32 | ## 打赏码 33 | 34 | **微信赞赏最高:200¥,其余金额可以通过爱发电平台支持,谢谢。** 35 | 36 | | 微信 | 微信赞赏 | 支付宝 | 37 | | :-------------------------------------------------------------------------------------: | :---------------------------------------------------------------------: | :---------------------------------------------------------------------: | 38 | | ![微信收款](https://cdn.upyun.sugarat.top/mdImg/sugar/5a8abb55ed888b133e514e613b0af68e) | ![](https://img.cdn.sugarat.top/mdImg/MTY0Nzc1NTYyOTE5Mw==647755629193) | ![](https://img.cdn.sugarat.top/mdImg/MTY1MTU0NzQyOTg0OA==651547429848) | 39 | 40 | ## 爱发电 41 | 42 | **爱发电主页:https://afdian.com/a/sugarat** 43 | 44 | ![](https://cdn.upyun.sugarat.top/mdImg/sugar/6677cb36b2706d1920073073288ce42a) 45 | 46 | ## 打赏记录 47 | 48 | **累计:1377.59¥** 49 | 50 | | 昵称 | 日期 | 来源 | 金额 | 备注 | 51 | | ------------ | ---------- | ------ | ------ | -------------------------------- | 52 | | Sak\*\*\*iro | 2022-06-18 | 微信 | 6.66 | 加油 | 53 | | 欲\*\*\*\*熊 | 2022-07-12 | 微信 | 6.66 | 我是大废物 | 54 | | 周\*\*\*\*琪 | 2022-07-31 | QQ | 35.00 | 请你喝奶茶 | 55 | | \*了 | 2022-08-31 | 微信 | 6.66 | 加油大哥 | 56 | | 欲\*\*\*\*熊 | 2022-09-12 | 微信 | 20.00 | 手动狗头 发电 | 57 | | 匿名 | 2022-09-27 | 微信 | 20.00 | 辛苦了,喝杯奶茶 | 58 | | 太\*\*\*谦 | 2022-10-23 | 微信 | 6.66 | 加油💪🏻 | 59 | | \*凌 | 2023-01-22 | 微信 | 6.66 | 非常好的程序,加油 | 60 | | \*L | 2023-01-31 | 微信 | 6.66 | - | 61 | | 匿名 | 2023-03-08 | 微信 | 10.00 | 老大辛苦啦 | 62 | | 匿名 | 2023-06-16 | 微信 | 6.66 | - | 63 | | 有南 | 2023-07-06 | 微信 | 52 | - | 64 | | 琥\*\*\*\*月 | 2023-07-23 | 微信 | 3 | 很厉害!加油! | 65 | | D\*\*\*\*e | 2023-10-04 | 微信 | 66 | 感谢大佬 | 66 | | 张\*\*\*\*y | 2023-10-22 | 微信 | 200 | 加油!努力把这个开源项目搞大搞强 | 67 | | 调\*\*\*\*g | 2023-10-25 | 微信 | 5 | 来瓶元气森林 | 68 | | 匿名 | 2023-12-10 | 微信 | 6.66 | - | 69 | | 闽\*\*\*\*厅 | 2024-03-28 | 微信 | 50 | - | 70 | | \*\*\*\*龙 | 2024-04-01 | 支付宝 | 6.6 | 发电发电 | 71 | | L\*\*\*a | 2024-04-15 | 爱发电 | 200 | 下馆子 | 72 | | 喵\*\* | 2024-05-30 | 微信 | 3 | 完美解决机构文件收集问题 | 73 | | 林\*\* | 2024-06-13 | 微信 | 3 | 谢谢作者大大 | 74 | | E\*\*🐻 | 2024-06-19 | 微信 | 6.66 | - | 75 | | E\*\*🐻 | 2024-06-24 | 爱发电 | 50 | - | 76 | | 龙\*\*n | 2024-06-26 | 微信 | 2 | - | 77 | | \*\*泽 | 2024-06-26 | 微信 | 77.8 | - | 78 | | \*\*泽 | 2024-07-01 | 微信 | 77.8 | - | 79 | | \*\*泽 | 2024-07-09 | 微信 | 109.8 | - | 80 | | \*\*泽 | 2024-07-15 | 微信 | 27.4 | - | 81 | | \*\*juXe | 2024-08-03 | 爱发电 | 50 | - | 82 | | \*\*L | 2024-09-07 | 微信 | 66.66 | - | 83 | | \*\*熠 | 2024-09-09 | 微信 | 125.93 | - | 84 | | S\*\*u | 2024-10-25 | 微信 | 50 | - | 85 | | 。 | 2024-12-30 | 微信 | 6.66 | - | 86 | 87 | 再次感谢以上网友的支持 💐💐 88 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ATQQ/easypicker2-client/2a352d0999889e052eeb5e2248e3dae093bbb5a6/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ATQQ/easypicker2-client/2a352d0999889e052eeb5e2248e3dae093bbb5a6/docs/public/group.png -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ATQQ/easypicker2-client/2a352d0999889e052eeb5e2248e3dae093bbb5a6/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /docs/src/apis/ajax.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { ElMessage } from 'element-plus' 3 | 4 | const instance = axios.create({ 5 | baseURL: import.meta.env.VITE_APP_AXIOS_BASE_URL, 6 | }) 7 | 8 | /** 9 | * 请求拦截 10 | */ 11 | instance.interceptors.request.use((config) => { 12 | const { method, params } = config 13 | // 附带鉴权的token 14 | const headers: any = { 15 | 16 | } 17 | // 不缓存get请求 18 | if (method === 'get') { 19 | headers['Cache-Control'] = 'no-cache' 20 | } 21 | // delete请求参数放入body中 22 | if (method === 'delete') { 23 | headers['Content-type'] = 'application/json;' 24 | Object.assign(config, { 25 | data: params, 26 | params: {}, 27 | }) 28 | } 29 | 30 | return ({ 31 | ...config, 32 | headers, 33 | }) 34 | }) 35 | 36 | // 跳转首页防抖 37 | let redirectHome = true 38 | /** 39 | * 响应拦截 40 | */ 41 | instance.interceptors.response.use((v) => { 42 | if (v.status === 200) { 43 | if (v.data.code === 0) { 44 | return v.data 45 | } 46 | if (v.data?.code === 3004) { 47 | if (redirectHome) { 48 | redirectHome = false 49 | ElMessage.error('登录过期,跳转首页') 50 | setTimeout(() => { 51 | redirectHome = true 52 | }, 1000) 53 | } 54 | } 55 | if (v?.data?.code === 500) { 56 | ElMessage.error(v?.data?.msg) 57 | } 58 | return Promise.reject(v.data) 59 | } 60 | ElMessage.error(v.statusText) 61 | return Promise.reject(v) 62 | }, (err) => { 63 | ElMessage.error(`网络错误:${err}`) 64 | return Promise.reject(err) 65 | }) 66 | export default instance 67 | -------------------------------------------------------------------------------- /docs/src/apis/index.ts: -------------------------------------------------------------------------------- 1 | export { default as WishApi } from './modules/wish' 2 | -------------------------------------------------------------------------------- /docs/src/apis/modules/wish.ts: -------------------------------------------------------------------------------- 1 | import ajax from '../ajax' 2 | 3 | function addWish(wish:Partial):WishApiTypes.addWish { 4 | return ajax.post( 5 | '/wish/add', wish, 6 | ) 7 | } 8 | 9 | function getDocsWish():WishApiTypes.allDocsWishData { 10 | return ajax.get('wish/all/docs') 11 | } 12 | 13 | function praiseWish(id:string) { 14 | return ajax.post(`wish/praise/${id}`) 15 | } 16 | export default { 17 | addWish, 18 | getDocsWish, 19 | praiseWish, 20 | } 21 | -------------------------------------------------------------------------------- /docs/src/components/Avatar.vue: -------------------------------------------------------------------------------- 1 | 21 | 70 | -------------------------------------------------------------------------------- /docs/src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 43 | 47 | 48 | 92 | -------------------------------------------------------------------------------- /docs/src/components/Picture.vue: -------------------------------------------------------------------------------- 1 | 4 | 14 | -------------------------------------------------------------------------------- /docs/src/components/Praise.vue: -------------------------------------------------------------------------------- 1 | 27 | 46 | 55 | -------------------------------------------------------------------------------- /docs/src/components/WishBtn.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 91 | 92 | 99 | -------------------------------------------------------------------------------- /docs/src/components/WishPanel.vue: -------------------------------------------------------------------------------- 1 | 66 | 115 | 171 | -------------------------------------------------------------------------------- /docs/src/components/callme/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 41 | 73 | -------------------------------------------------------------------------------- /docs/vite.config.mts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { defineConfig } from 'vite' 3 | import Components from 'unplugin-vue-components/vite' 4 | import AutoImport from 'unplugin-auto-import/vite' 5 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | Components({ 11 | include: [/\.vue/, /\.md/], 12 | dts: true, 13 | }), 14 | AutoImport({ 15 | resolvers: [ElementPlusResolver()], 16 | }), 17 | Components({ 18 | resolvers: [ElementPlusResolver()], 19 | }), 20 | ], 21 | optimizeDeps: { 22 | include: ['vue', 'element-plus'], 23 | }, 24 | server: { 25 | proxy: { 26 | '/api/': { 27 | target: 'http://localhost:3000', 28 | changeOrigin: true, 29 | rewrite: p => p.replace(/^\/api/, ''), 30 | }, 31 | }, 32 | }, 33 | resolve: { 34 | alias: { 35 | '@': path.resolve(__dirname, './../src'), 36 | '@components': path.resolve(__dirname, './../src/components'), 37 | }, 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | formatters: true, 5 | vue: true, 6 | rules: { 7 | 'no-console': 'off', 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | EasyPicker-轻取 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 114 | 115 | 118 | 119 | 120 | 121 | 122 | 123 |
124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sugarat/easypicker2-client", 3 | "version": "2.7.3", 4 | "files": [ 5 | "dist", 6 | "package.json" 7 | ], 8 | "scripts": { 9 | "dev": "vite", 10 | "dev:test": "cross-env VITE_APP_AXIOS_BASE_URL=/api-test/ vite --mode test", 11 | "build": "vite build", 12 | "build:test": "cross-env VITE_APP_AXIOS_BASE_URL=/api-test/ vite build --mode test", 13 | "serve": "vite preview", 14 | "docs:dev": "vitepress dev docs", 15 | "docs:build": "vitepress build docs", 16 | "docs:serve": "vitepress serve docs", 17 | "docs:deploy": "zx scripts/deploy/docs.mjs", 18 | "deploy:prod": "zx scripts/deploy/prod.mjs", 19 | "deploy:test": "zx scripts/deploy/test.mjs", 20 | "upload:oss": "pnpm build && q ep client -up", 21 | "update:version": "npm version prerelease --preid=beta --no-git-tag-version", 22 | "preinstall": "npm config set registry https://registry.npmmirror.com/", 23 | "lint": "eslint .", 24 | "lint:fix": "eslint . --fix" 25 | }, 26 | "dependencies": { 27 | "@element-plus/icons-vue": "^1.1.4", 28 | "@sugarat/theme": "^0.5.3", 29 | "@vueuse/core": "^10.11.0", 30 | "axios": "^0.27.2", 31 | "clipboard-copy": "^4.0.1", 32 | "element-plus": "2.2.13", 33 | "spark-md5": "^3.0.2", 34 | "vitepress": "1.5.0", 35 | "vitepress-plugin-51la": "^0.1.0", 36 | "vue": "^3.4.31", 37 | "vue-json-viewer": "^3.0.4", 38 | "vue-router": "^4.4.0", 39 | "vuex": "^4.1.0" 40 | }, 41 | "devDependencies": { 42 | "@antfu/eslint-config": "^2.21.2", 43 | "@types/node": "20", 44 | "@vitejs/plugin-legacy": "^5.4.1", 45 | "@vitejs/plugin-vue": "^5.0.5", 46 | "cross-env": "^7.0.3", 47 | "eslint": "^9.6.0", 48 | "lint-staged": "^15.2.7", 49 | "pagefind": "^1.3.0", 50 | "sass": "^1.64.1", 51 | "simple-git-hooks": "^2.11.1", 52 | "terser": "^5.19.2", 53 | "typescript": "^4.9.5", 54 | "unplugin-auto-import": "^0.6.9", 55 | "unplugin-element-plus": "^0.4.1", 56 | "unplugin-vue-components": "^0.27.2", 57 | "vite": "^5.3.2" 58 | }, 59 | "simple-git-hooks": { 60 | "pre-commit": "pnpm lint-staged" 61 | }, 62 | "lint-staged": { 63 | "*": "eslint --fix" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ATQQ/easypicker2-client/2a352d0999889e052eeb5e2248e3dae093bbb5a6/public/favicon.ico -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ATQQ/easypicker2-client/2a352d0999889e052eeb5e2248e3dae093bbb5a6/public/logo.png -------------------------------------------------------------------------------- /scripts/deploy/docs.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | // user config 4 | const originName = 'docs.ep' 5 | 6 | // not care 7 | const compressPkgName = `${originName}.tar.gz` 8 | const user = 'root' 9 | const origin = 'sugarat.top' 10 | const fullOrigin = `${originName}.${origin}` 11 | const baseServerDir = '/www/wwwroot' 12 | const destDir = '' 13 | 14 | await $`pnpm docs:build` 15 | 16 | await $`echo ==🔧 压缩dist ==` 17 | await $`cd docs/.vitepress && tar -zvcf ${compressPkgName} dist && rm -rf dist && mv ${compressPkgName} ./../../` 18 | 19 | await $`echo ==🚀 上传到服务器 ==` 20 | await $`scp ${compressPkgName} ${user}@${fullOrigin}:./` 21 | await $`rm -rf ${compressPkgName}` 22 | 23 | await $`echo ==✅ 部署代码 ==` 24 | await $`ssh -p22 ${user}@${fullOrigin} "tar -xf ${compressPkgName} -C ${baseServerDir}/${fullOrigin}/${destDir}"` 25 | -------------------------------------------------------------------------------- /scripts/deploy/prod.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | // user config 4 | const originName = 'ep' 5 | 6 | // not care 7 | const compressPkgName = `${originName}.tar.gz` 8 | const user = 'root' 9 | const origin = 'sugarat.top' 10 | const fullOrigin = `${originName}.${origin}` 11 | const baseServerDir = '/www/wwwroot' 12 | const destDir = '' 13 | 14 | await $`pnpm build` 15 | 16 | await $`echo ==🔧 压缩dist ==` 17 | await $`tar -zvcf ${compressPkgName} dist && rm -rf dist` 18 | 19 | await $`echo ==🚀 上传到服务器 ==` 20 | await $`scp ${compressPkgName} ${user}@${origin}:./` 21 | await $`rm -rf ${compressPkgName}` 22 | 23 | await $`echo ==✅ 部署代码 ==` 24 | await $`ssh -p22 ${user}@${origin} "tar -xf ${compressPkgName} -C ${baseServerDir}/${fullOrigin}/${destDir}"` 25 | -------------------------------------------------------------------------------- /scripts/deploy/test.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | /* eslint-disable no-await-in-loop */ 3 | 4 | // user config 5 | const originName = ['ep.test', 'ep.dev'] 6 | 7 | // not care 8 | const compressPkgName = `${originName}.tar.gz` 9 | const user = 'root' 10 | const origin = 'sugarat.top' 11 | const baseServerDir = '/www/wwwroot' 12 | const destDir = '' 13 | 14 | await $`pnpm build` 15 | 16 | await $`echo ==🔧 压缩dist ==` 17 | await $`tar -zvcf ${compressPkgName} dist && rm -rf dist` 18 | 19 | await $`echo ==🚀 上传到服务器 ==` 20 | await $`scp ${compressPkgName} ${user}@${origin}:./` 21 | await $`rm -rf ${compressPkgName}` 22 | 23 | await $`echo ==✅ 部署代码 ==` 24 | for (const name of originName) { 25 | await $`ssh -p22 ${user}@${origin} "tar -xf ${compressPkgName} -C ${baseServerDir}/${name}.${origin}/${destDir}"` 26 | } 27 | -------------------------------------------------------------------------------- /src/@types/ajax.d.ts: -------------------------------------------------------------------------------- 1 | interface BaseResponse { 2 | code: number 3 | errMsg: string 4 | data: T 5 | } 6 | -------------------------------------------------------------------------------- /src/@types/lib.d.ts: -------------------------------------------------------------------------------- 1 | // 第三方库的类型定义 2 | declare namespace qiniu { 3 | interface Subscription { 4 | unsubscribe(): void 5 | } 6 | 7 | interface SubscriptionConfig { 8 | next(res: any): void 9 | error(err: any): void 10 | complete(res: any): void 11 | } 12 | interface Observable { 13 | subscribe(cf: SubscriptionConfig): Subscription 14 | } 15 | 16 | type upload = (file: File, key: string, token: string) => Observable 17 | const upload: upload 18 | } 19 | 20 | declare namespace XLSX { 21 | interface utils { 22 | table_to_book: (dom: HTMLElement) => any 23 | } 24 | const utils: utils 25 | const writeFile: (wb: any, filename: string) => void 26 | } 27 | -------------------------------------------------------------------------------- /src/@types/page.d.ts: -------------------------------------------------------------------------------- 1 | interface DownloadItem { 2 | url: string 3 | filename: string 4 | mimeType: string 5 | status: 'ready' | 'downloading' | 'done' | 'error' 6 | percentage: number 7 | size: number 8 | } 9 | 10 | type InfoItemType = 'input' | 'radio' | 'text' | 'select' 11 | interface InfoItem { 12 | type?: InfoItemType 13 | // 描述信息 14 | text?: string 15 | // 表单项的值 16 | value?: string 17 | children?: InfoItem[] 18 | } 19 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | 29 | 41 | -------------------------------------------------------------------------------- /src/apis/ajax.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { ElMessage } from 'element-plus' 3 | import router from '@/router' 4 | 5 | const instance = axios.create({ 6 | baseURL: import.meta.env.VITE_APP_AXIOS_BASE_URL 7 | }) 8 | 9 | /** 10 | * 请求拦截 11 | */ 12 | instance.interceptors.request.use((config) => { 13 | const { method, params } = config 14 | const token = localStorage.getItem('token') 15 | // 附带鉴权的token 16 | const headers: any = {} 17 | if (token) { 18 | headers.token = token 19 | } 20 | // 不缓存get请求 21 | if (method === 'get') { 22 | headers['Cache-Control'] = 'no-cache' 23 | } 24 | // delete请求参数放入body中 25 | if (method === 'delete') { 26 | headers['Content-type'] = 'application/json;' 27 | Object.assign(config, { 28 | data: params, 29 | params: {} 30 | }) 31 | } 32 | 33 | return { 34 | ...config, 35 | headers 36 | } 37 | }) 38 | 39 | /** 40 | * 响应拦截 41 | */ 42 | instance.interceptors.response.use( 43 | (v) => { 44 | if (v.status === 200) { 45 | if (v.data.code === 0) { 46 | return v.data 47 | } 48 | if (v.data?.code === 3004 && router.currentRoute.value.name !== 'login') { 49 | localStorage.removeItem('token') 50 | localStorage.removeItem('system') 51 | ElMessage.error('登录过期,跳转登录') 52 | router.replace({ 53 | name: 'login', 54 | query: { 55 | redirect: router.currentRoute.value.fullPath 56 | } 57 | }) 58 | } 59 | if (v?.data?.code === 500) { 60 | ElMessage.error(v?.data?.msg) 61 | } 62 | return Promise.reject(v.data) 63 | } 64 | ElMessage.error(v.statusText) 65 | return Promise.reject(v) 66 | }, 67 | (err) => { 68 | ElMessage.error(`网络错误:${err}`) 69 | return Promise.reject(err) 70 | } 71 | ) 72 | export default instance 73 | -------------------------------------------------------------------------------- /src/apis/index.ts: -------------------------------------------------------------------------------- 1 | import p from './modules/public' 2 | import user from './modules/user' 3 | import category from './modules/category' 4 | import task from './modules/task' 5 | import people from './modules/people' 6 | import file from './modules/file' 7 | import superOverview from './modules/super/overview' 8 | import superUser from './modules/super/user' 9 | 10 | export const PublicApi = p 11 | export const UserApi = user 12 | export const CategoryApi = category 13 | export const TaskApi = task 14 | export const PeopleApi = people 15 | export const FileApi = file 16 | export const SuperOverviewApi = superOverview 17 | export const SuperUserApi = superUser 18 | export { default as WishApi } from './modules/wish' 19 | export { default as ConfigServiceAPI } from './modules/config' 20 | export { default as ActionServiceAPI } from './modules/action' 21 | -------------------------------------------------------------------------------- /src/apis/modules/action.ts: -------------------------------------------------------------------------------- 1 | import ajax from '../ajax' 2 | 3 | function getDownloadActions( 4 | pageSize: number, 5 | pageIndex: number, 6 | extraIds: string[] = [] 7 | ): ActionApiTypes.getDownloadActions { 8 | return ajax.post('/action/download/list', { 9 | pageSize, 10 | pageIndex, 11 | extraIds 12 | }) 13 | } 14 | 15 | export default { 16 | getDownloadActions 17 | } 18 | -------------------------------------------------------------------------------- /src/apis/modules/category.ts: -------------------------------------------------------------------------------- 1 | import ajax from '../ajax' 2 | 3 | function getList(): CateGoryApiTypes.getList { 4 | return ajax.get('category') 5 | } 6 | 7 | function createNew(name: string): CateGoryApiTypes.createNew { 8 | return ajax.post('category/create', { 9 | name 10 | }) 11 | } 12 | 13 | function deleteOne(key: string): CateGoryApiTypes.deleteOne { 14 | return ajax.delete(`category/${key}`) 15 | } 16 | export default { 17 | getList, 18 | createNew, 19 | deleteOne 20 | } 21 | -------------------------------------------------------------------------------- /src/apis/modules/config.ts: -------------------------------------------------------------------------------- 1 | import ajax from '../ajax' 2 | 3 | function getServiceOverview(): ConfigServiceAPITypes.getServiceOverview { 4 | return ajax.get('/config/service/overview') 5 | } 6 | 7 | function getServiceConfig(): ConfigServiceAPITypes.getServiceConfig { 8 | return ajax.get('/config/service/config') 9 | } 10 | 11 | function updateCfg(data: ConfigServiceAPITypes.ServiceConfigItem) { 12 | return ajax.put('/config/service/config', data) 13 | } 14 | 15 | export default { 16 | getServiceOverview, 17 | getServiceConfig, 18 | updateCfg 19 | } 20 | -------------------------------------------------------------------------------- /src/apis/modules/file.ts: -------------------------------------------------------------------------------- 1 | import ajax from '../ajax' 2 | 3 | function getUploadToken(): FileApiTypes.getUploadToken { 4 | return ajax.get('file/token') 5 | } 6 | 7 | function addFile(options: FileApiTypes.FileOptions): FileApiTypes.addFile { 8 | return ajax.post('file/info', options) 9 | } 10 | 11 | function getFileList(): FileApiTypes.getFileList { 12 | return ajax.get('file/list') 13 | } 14 | 15 | function getTemplateUrl( 16 | template: string, 17 | key: string, 18 | ): FileApiTypes.getTemplateUrl { 19 | return ajax.get('file/template', { 20 | params: { 21 | template, 22 | key, 23 | }, 24 | }) 25 | } 26 | 27 | function getOneFileUrl(id: number): FileApiTypes.getOneFileUrl { 28 | return ajax.get('file/one', { 29 | params: { 30 | id, 31 | }, 32 | }) 33 | } 34 | 35 | function deleteOneFile(id: number): FileApiTypes.deleteOneFile { 36 | return ajax.delete('file/one', { 37 | params: { 38 | id, 39 | }, 40 | }) 41 | } 42 | 43 | function batchDownload( 44 | ids: number[], 45 | zipName?: string, 46 | ): FileApiTypes.batchDownload { 47 | return ajax.post('file/batch/down', { 48 | ids, 49 | zipName, 50 | }) 51 | } 52 | 53 | function batchDel(ids: number[]): FileApiTypes.batchDel { 54 | return ajax.delete('file/batch/del', { 55 | params: { 56 | ids, 57 | }, 58 | }) 59 | } 60 | 61 | function checkCompressStatus(id: string): FileApiTypes.checkCompressStatus { 62 | return ajax.post('file/compress/status', { 63 | id, 64 | }) 65 | } 66 | function getCompressDownUrl(key: string): FileApiTypes.getCompressDownUrl { 67 | return ajax.post('file/compress/down', { 68 | key, 69 | }) 70 | } 71 | function getCompressFileUrl(id: string): Promise { 72 | const check = (_r: any, _rej) => { 73 | checkCompressStatus(id) 74 | .then((r) => { 75 | const { code, key } = r.data 76 | if (code === 0) { 77 | getCompressDownUrl(key ?? '').then((v) => { 78 | const { url } = v.data 79 | _r(url) 80 | }) 81 | } 82 | else { 83 | setTimeout(() => { 84 | check(_r, _rej) 85 | }, 1000) 86 | } 87 | }) 88 | .catch((err) => { 89 | _rej(err) 90 | }) 91 | } 92 | 93 | return new Promise((resolve, rej) => { 94 | check(resolve, rej) 95 | }) 96 | } 97 | 98 | function withdrawFile( 99 | options: FileApiTypes.WithdrawFileOptions, 100 | ): FileApiTypes.withdrawFile { 101 | return ajax.delete('file/withdraw', { 102 | params: options, 103 | }) 104 | } 105 | 106 | function checkSubmitStatus( 107 | taskKey: string, 108 | info: any, 109 | name = '', 110 | ): FileApiTypes.checkSubmitStatus { 111 | return ajax.post('file/submit/people', { 112 | taskKey, 113 | info, 114 | name, 115 | }) 116 | } 117 | 118 | function checkImageFilePreviewUrl( 119 | ids: number[], 120 | ): FileApiTypes.checkImageFilePreviewUrl { 121 | return ajax.post('file/image/preview', { 122 | ids, 123 | }) 124 | } 125 | 126 | function fileDownloadCount(ids: number[]) { 127 | return ajax.post('file/download/count', { 128 | ids, 129 | }) 130 | } 131 | 132 | function updateFilename( 133 | id: number, 134 | newName: string, 135 | ): FileApiTypes.updateFilename { 136 | return ajax.put('file/name/rewrite', { 137 | id, 138 | name: newName, 139 | }) 140 | } 141 | export default { 142 | getUploadToken, 143 | addFile, 144 | getFileList, 145 | getTemplateUrl, 146 | withdrawFile, 147 | getOneFileUrl, 148 | deleteOneFile, 149 | batchDownload, 150 | batchDel, 151 | checkCompressStatus, 152 | getCompressFileUrl, 153 | getCompressDownUrl, 154 | checkSubmitStatus, 155 | checkImageFilePreviewUrl, 156 | updateFilename, 157 | fileDownloadCount, 158 | } 159 | -------------------------------------------------------------------------------- /src/apis/modules/people.ts: -------------------------------------------------------------------------------- 1 | import ajax from '../ajax' 2 | 3 | function importPeople( 4 | key: string, 5 | filename: string, 6 | type: string 7 | ): PeopleApiTypes.importPeople { 8 | return ajax.post(`/people/${key}`, { 9 | filename, 10 | type 11 | }) 12 | } 13 | 14 | function getPeople(key: string, detail = '0'): PeopleApiTypes.getPeople { 15 | return ajax.get(`/people/${key}`, { 16 | params: { 17 | detail 18 | } 19 | }) 20 | } 21 | 22 | function deletePeople(key: string, id: number): PeopleApiTypes.deletePeople { 23 | return ajax.delete(`/people/${key}`, { 24 | params: { 25 | id 26 | } 27 | }) 28 | } 29 | 30 | function updatePeopleStatus( 31 | key: string, 32 | filename: string, 33 | name: string, 34 | hash: string 35 | ): PeopleApiTypes.updatePeopleStatus { 36 | return ajax.put(`/people/${key}`, { 37 | filename, 38 | name, 39 | hash 40 | }) 41 | } 42 | 43 | function checkPeopleIsExist( 44 | key: string, 45 | name: string 46 | ): PeopleApiTypes.checkPeopleIsExist { 47 | return ajax.post(`/people/check/${key}`, { 48 | name 49 | }) 50 | } 51 | 52 | function getUsefulTemplate(key: string): PeopleApiTypes.getUsefulTemplate { 53 | return ajax.get(`/people/template/${key}`) 54 | } 55 | 56 | function importPeopleFromTpl( 57 | taskKey: string, 58 | tplKey: string, 59 | type: string 60 | ): PeopleApiTypes.importFromTpl { 61 | return ajax.put(`/people/template/${taskKey}`, { 62 | key: tplKey, 63 | type 64 | }) 65 | } 66 | 67 | function addPeopleByUser(name: string, key: string) { 68 | return ajax.post(`/people/add/${key}`, { 69 | name 70 | }) 71 | } 72 | export default { 73 | importPeopleFromTpl, 74 | importPeople, 75 | getPeople, 76 | deletePeople, 77 | updatePeopleStatus, 78 | checkPeopleIsExist, 79 | getUsefulTemplate, 80 | addPeopleByUser 81 | } 82 | -------------------------------------------------------------------------------- /src/apis/modules/public.ts: -------------------------------------------------------------------------------- 1 | import ajax from '../ajax' 2 | 3 | /** 4 | * 获取验证码 5 | * @param mobile 手机号 6 | */ 7 | function getCode(phone: string): PublicApiTypes.getCode { 8 | return ajax.get('public/code', { 9 | params: { 10 | phone 11 | } 12 | }) 13 | } 14 | 15 | function reportPv(path: string): PublicApiTypes.reportPv { 16 | return ajax.post('public/report/pv', { 17 | path 18 | }) 19 | } 20 | 21 | function checkPhone(phone: string): PublicApiTypes.checkPhone { 22 | return ajax.get('public/check/phone', { 23 | params: { 24 | phone 25 | } 26 | }) 27 | } 28 | 29 | function getTipImageUrl( 30 | key: string, 31 | data: { 32 | uid: number 33 | name: string 34 | }[] 35 | ) { 36 | return ajax.post>( 37 | 'public/tip/image', 38 | { 39 | key, 40 | data 41 | } 42 | ) 43 | } 44 | export default { 45 | getCode, 46 | reportPv, 47 | checkPhone, 48 | getTipImageUrl 49 | } 50 | -------------------------------------------------------------------------------- /src/apis/modules/super/overview.ts: -------------------------------------------------------------------------------- 1 | import ajax from '../../ajax' 2 | import { mergeRequest } from '@/utils/networkUtil' 3 | 4 | const baseUrl = '/super/overview' 5 | function getCount(): OverviewApiTypes.getCount { 6 | return ajax.get(`${baseUrl}/count`) 7 | } 8 | 9 | function getAllLogMsg(): OverviewApiTypes.getAllLogMsg { 10 | return ajax.get(`${baseUrl}/log`) 11 | } 12 | 13 | function getLogMsg( 14 | pageSize: number, 15 | pageIndex: number, 16 | type: string, 17 | search: string, 18 | ): OverviewApiTypes.getLogMsg { 19 | return ajax.post(`${baseUrl}/log`, { 20 | pageSize, 21 | pageIndex, 22 | type, 23 | search, 24 | }) 25 | } 26 | 27 | function getLogMsgDetail(id: string): any { 28 | return ajax.get(`${baseUrl}/log/${id}`) 29 | } 30 | 31 | function clearExpiredCompressFile() { 32 | return ajax.delete(`${baseUrl}/compress`) 33 | } 34 | 35 | function _checkDisabledRoute(route: string): OverviewApiTypes.disabledStatus { 36 | return ajax.get(`${baseUrl}/route/disabled`, { 37 | params: { 38 | route, 39 | }, 40 | }) 41 | } 42 | 43 | const checkDisabledRoute = mergeRequest(_checkDisabledRoute) 44 | 45 | function addDisabledRoute(route: string, status: boolean) { 46 | return ajax.post(`${baseUrl}/route/disabled`, { 47 | route, 48 | status, 49 | }) 50 | } 51 | 52 | function getGlobalConfig(type = 'site'): OverviewApiTypes.getGlobalConfig { 53 | return ajax.get(`/config/global`, { params: { type } }) 54 | } 55 | 56 | function getGlobalAllConfig(type = 'site'): OverviewApiTypes.getGlobalConfig { 57 | return ajax.get(`/config/global/all`, { params: { type } }) 58 | } 59 | 60 | function updateGlobalConfig(key: string, value: any) { 61 | return ajax.put(`/config/global`, { key, value }) 62 | } 63 | 64 | export default { 65 | getCount, 66 | getAllLogMsg, 67 | getLogMsg, 68 | getLogMsgDetail, 69 | clearExpiredCompressFile, 70 | checkDisabledRoute, 71 | addDisabledRoute, 72 | getGlobalConfig, 73 | updateGlobalConfig, 74 | getGlobalAllConfig, 75 | } 76 | -------------------------------------------------------------------------------- /src/apis/modules/super/user.ts: -------------------------------------------------------------------------------- 1 | import ajax from '../../ajax' 2 | 3 | const baseUrl = '/super/user' 4 | function getUserList(): SuperUserApiTypes.getUserList { 5 | return ajax.get(`${baseUrl}/list`) 6 | } 7 | 8 | function updateUserStatus(id: number, status: number, openTime: string) { 9 | return ajax.put(`${baseUrl}/status`, { 10 | id, 11 | status, 12 | openTime, 13 | }) 14 | } 15 | function resetPassword(id: number, password: string) { 16 | return ajax.put(`${baseUrl}/password`, { 17 | id, 18 | password, 19 | }) 20 | } 21 | 22 | function resetPhone(id: number, phone: string, code: string) { 23 | return ajax.put(`${baseUrl}/phone`, { 24 | id, 25 | phone, 26 | code, 27 | }) 28 | } 29 | 30 | function clearOssFile(id: number, type: string) { 31 | return ajax.delete(`${baseUrl}/clear/oss`, { 32 | params: { id, type }, 33 | }) 34 | } 35 | 36 | function getMessageList(): SuperUserApiTypes.getMessageList { 37 | return ajax.get(`${baseUrl}/message`) 38 | } 39 | 40 | function readMessage(id: string) { 41 | return ajax.put(`${baseUrl}/message`, { 42 | id, 43 | }) 44 | } 45 | 46 | function sendMessage(text: string, type: number, target?: number) { 47 | return ajax.post(`${baseUrl}/message`, { 48 | text, 49 | type, 50 | target, 51 | }) 52 | } 53 | function logout(account: string) { 54 | return ajax.delete(`${baseUrl}/logout`, { 55 | params: { account }, 56 | }) 57 | } 58 | 59 | function resetLimitSpace(id: number, size: number) { 60 | return ajax.put(`${baseUrl}/size`, { 61 | id, 62 | size, 63 | }) 64 | } 65 | 66 | function updateWallet(id: number, value: number) { 67 | return ajax.put(`${baseUrl}/wallet`, { 68 | id, 69 | value, 70 | }) 71 | } 72 | 73 | export default { 74 | getUserList, 75 | updateUserStatus, 76 | resetPassword, 77 | resetPhone, 78 | clearOssFile, 79 | getMessageList, 80 | readMessage, 81 | sendMessage, 82 | logout, 83 | resetLimitSpace, 84 | updateWallet, 85 | } 86 | -------------------------------------------------------------------------------- /src/apis/modules/task.ts: -------------------------------------------------------------------------------- 1 | import ajax from '../ajax' 2 | 3 | function getList(): TaskApiTypes.getList { 4 | return ajax.get('task') 5 | } 6 | 7 | function create(name: string, category: string): TaskApiTypes.create { 8 | return ajax.post('task/create', { 9 | name, 10 | category 11 | }) 12 | } 13 | 14 | function deleteOne(key: string): TaskApiTypes.deleteOne { 15 | return ajax.delete(`task/${key}`) 16 | } 17 | 18 | function updateBaseInfo( 19 | key: string, 20 | name: string, 21 | category: string 22 | ): TaskApiTypes.updateBaseInfo { 23 | return ajax.put(`task/${key}`, { 24 | name, 25 | category 26 | }) 27 | } 28 | 29 | function getTaskInfo(key: string): TaskApiTypes.getTaskInfo { 30 | return ajax.get(`task/${key}`) 31 | } 32 | 33 | function getTaskMoreInfo(key: string): TaskApiTypes.getTaskMoreInfo { 34 | return ajax.get(`task_info/${key}`) 35 | } 36 | 37 | function updateTaskMoreInfo( 38 | key: string, 39 | options: TaskApiTypes.TaskInfo 40 | ): TaskApiTypes.updateTaskMoreInfo { 41 | return ajax.put(`task_info/${key}`, options) 42 | } 43 | 44 | function getUsefulTemplate(key: string): TaskApiTypes.getUsefulTemplate { 45 | return ajax.get(`/task_info/template/${key}`) 46 | } 47 | 48 | function delTipImage(key: string, uid: number, name: string) { 49 | return ajax.delete(`/task_info/tip/image/${key}`, { 50 | params: { 51 | uid, 52 | name 53 | } 54 | }) 55 | } 56 | 57 | export default { 58 | getList, 59 | create, 60 | deleteOne, 61 | updateBaseInfo, 62 | getTaskInfo, 63 | getTaskMoreInfo, 64 | updateTaskMoreInfo, 65 | getUsefulTemplate, 66 | delTipImage 67 | } 68 | -------------------------------------------------------------------------------- /src/apis/modules/user.ts: -------------------------------------------------------------------------------- 1 | import ajax from '../ajax' 2 | 3 | function register( 4 | options: UserApiTypes.RegisterOptions 5 | ): UserApiTypes.register { 6 | return ajax.post('user/register', { 7 | ...options 8 | }) 9 | } 10 | 11 | function login(account: string, pwd: string): UserApiTypes.login { 12 | return ajax.post('user/login', { 13 | account, 14 | pwd 15 | }) 16 | } 17 | 18 | function codeLogin(phone: string, code: string): UserApiTypes.codeLogin { 19 | return ajax.post('user/login/code', { 20 | phone, 21 | code 22 | }) 23 | } 24 | 25 | function resetPwd( 26 | phone: string, 27 | code: string, 28 | pwd: string 29 | ): UserApiTypes.resetPwd { 30 | return ajax.put('user/password', { 31 | phone, 32 | code, 33 | pwd 34 | }) 35 | } 36 | 37 | function checkPower(): UserApiTypes.checkPower { 38 | return ajax.get('user/power/super') 39 | } 40 | 41 | function checkLoginStatus(): UserApiTypes.checkLoginStatus { 42 | return ajax.get('user/login') 43 | } 44 | 45 | function logout(): UserApiTypes.logout { 46 | return ajax.get('user/logout') 47 | } 48 | 49 | function usage(): UserApiTypes.usage { 50 | return ajax.get('user/usage') 51 | } 52 | 53 | export default { 54 | register, 55 | login, 56 | codeLogin, 57 | resetPwd, 58 | checkPower, 59 | checkLoginStatus, 60 | logout, 61 | usage 62 | } 63 | -------------------------------------------------------------------------------- /src/apis/modules/wish.ts: -------------------------------------------------------------------------------- 1 | import { WishStatus } from '@/constants' 2 | import ajax from '../ajax' 3 | 4 | function addWish(wish: Partial): WishApiTypes.addWish { 5 | return ajax.post('/wish/add', wish) 6 | } 7 | 8 | function findAllWish(): WishApiTypes.allWishData { 9 | return ajax.get('/wish/all') 10 | } 11 | 12 | function updateWishStatus( 13 | id: string, 14 | status: WishStatus 15 | ): WishApiTypes.updateWish { 16 | return ajax.put('/wish/update', { id, status }) 17 | } 18 | 19 | function updateWishDes( 20 | id: string, 21 | title: string, 22 | des: string 23 | ): WishApiTypes.updateWish { 24 | return ajax.put(`/wish/update/${id}`, { title, des }) 25 | } 26 | export default { 27 | addWish, 28 | findAllWish, 29 | updateWishStatus, 30 | updateWishDes 31 | } 32 | -------------------------------------------------------------------------------- /src/assets/i/EasyPicker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ATQQ/easypicker2-client/2a352d0999889e052eeb5e2248e3dae093bbb5a6/src/assets/i/EasyPicker.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ATQQ/easypicker2-client/2a352d0999889e052eeb5e2248e3dae093bbb5a6/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/styles/app.css: -------------------------------------------------------------------------------- 1 | /* element ui 重写 */ 2 | .el-message__content { 3 | width: 325px; 4 | word-break: break-all; 5 | line-height: 1.5; 6 | } 7 | .el-progress { 8 | margin-top: 0 !important; 9 | position: static !important; 10 | } 11 | .el-progress__text { 12 | top: 8px !important; 13 | } 14 | 15 | .el-dialog { 16 | max-width: 766px; 17 | } 18 | 19 | .el-select-dropdown { 20 | max-width: 200px; 21 | } 22 | 23 | .el-card { 24 | max-width: 400px; 25 | } 26 | @media screen and (max-width: 700px) { 27 | .el-message-box { 28 | width: auto; 29 | max-width: 300px; 30 | } 31 | .el-pagination { 32 | flex-wrap: wrap; 33 | justify-content: center; 34 | } 35 | .el-pagination > * { 36 | margin-bottom: 10px !important; 37 | } 38 | } 39 | 40 | /* 一些高频公共样式 */ 41 | .flex { 42 | display: flex; 43 | } 44 | 45 | .fc { 46 | justify-content: center; 47 | } 48 | 49 | .fac { 50 | align-items: center; 51 | } 52 | 53 | .tc { 54 | text-align: center; 55 | } 56 | .p10 { 57 | padding: 10px; 58 | } 59 | 60 | .ellipsis { 61 | text-overflow: ellipsis; 62 | white-space: nowrap; 63 | overflow: hidden; 64 | } 65 | .ellipsis label { 66 | text-overflow: ellipsis; 67 | white-space: nowrap; 68 | overflow: hidden; 69 | } 70 | -------------------------------------------------------------------------------- /src/components/HomeFooter/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 96 | 97 | 135 | -------------------------------------------------------------------------------- /src/components/HomeHeader/index.vue: -------------------------------------------------------------------------------- 1 | 64 | 93 | 156 | -------------------------------------------------------------------------------- /src/components/InfosForm/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 61 | 62 | 81 | -------------------------------------------------------------------------------- /src/components/MessageList/index.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 106 | 107 | 151 | 152 | 162 | -------------------------------------------------------------------------------- /src/components/MessagePanel/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 42 | 47 | -------------------------------------------------------------------------------- /src/components/Praise/index.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 96 | 97 | 120 | -------------------------------------------------------------------------------- /src/components/QrCode.vue: -------------------------------------------------------------------------------- 1 | 4 | 45 | -------------------------------------------------------------------------------- /src/components/linkDialog.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 126 | 127 | 133 | -------------------------------------------------------------------------------- /src/components/loginPanel.vue: -------------------------------------------------------------------------------- 1 | 16 | 37 | 38 | 93 | -------------------------------------------------------------------------------- /src/composables/auth.ts: -------------------------------------------------------------------------------- 1 | import { onMounted } from 'vue' 2 | import { useLocalStorage } from '@vueuse/core' 3 | import { SuperOverviewApi } from '@/apis' 4 | 5 | export function useSupportRegister() { 6 | const supportRegister = useLocalStorage('supportRegister', true) 7 | // 检查注册功能是否禁用 8 | onMounted(() => { 9 | SuperOverviewApi.checkDisabledRoute('/register').then((v) => { 10 | supportRegister.value = !v.data.status 11 | }) 12 | }) 13 | return supportRegister 14 | } 15 | -------------------------------------------------------------------------------- /src/composables/form.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@vueuse/core' 2 | import { computed, onMounted } from 'vue' 3 | import { SuperOverviewApi } from '@/apis' 4 | import { formatDate } from '@/utils/stringUtil' 5 | 6 | export function useSiteConfig() { 7 | const value = useLocalStorage('siteConfig', { 8 | maxInputLength: 20, // 最大输入长度 9 | openPraise: false, // 是否开启赞赏相关提示文案 10 | formLength: 10, // 表单项数量 11 | compressSizeLimit: 10, // TODO: 压缩文件大小限制(GB) 12 | needBindPhone: false, // 是否需要绑定手机号 13 | limitSpace: false, // 是否限制空间 14 | limitWallet: false, // 是否限制费用 15 | moneyStartDay: +new Date('2024/11/01'), 16 | appName: '轻取', // 应用名称 17 | }) 18 | 19 | const moneyStartDay = computed(() => formatDate(value.value.moneyStartDay)) 20 | onMounted(() => { 21 | SuperOverviewApi.getGlobalConfig('site').then((res) => { 22 | value.value = res.data 23 | }) 24 | }) 25 | 26 | return { 27 | value, 28 | moneyStartDay, 29 | } 30 | } 31 | 32 | export function useSiteAllConfig() { 33 | const value = useLocalStorage('siteConfig', { 34 | maxInputLength: 20, // 最大输入长度 35 | openPraise: false, // 是否开启赞赏相关提示文案 36 | formLength: 10, // 表单项数量 37 | downloadOneExpired: 1, // 单个文件链接下载过期时间(min) 38 | downloadCompressExpired: 60, // 归档文件下载过期时间(min) 39 | compressSizeLimit: 10, // TODO: 压缩文件大小限制(GB) 40 | needBindPhone: false, // 是否需要绑定手机号 41 | limitSpace: false, // 是否限制空间 42 | qiniuOSSPrice: 0.099, // 七牛云存储价格 43 | qiniuCDNPrice: 0.28, // 七牛云CDN价格 44 | qiniuBackhaulTrafficPrice: 0.15, // 七牛云回源流量价格 45 | qiniuBackhaulTrafficPercentage: 0.8, // 七牛云回源流量占比 46 | qiniuCompressPrice: 0.05, // 七牛云压缩价格 47 | }) 48 | 49 | onMounted(() => { 50 | SuperOverviewApi.getGlobalAllConfig('site').then((res) => { 51 | value.value = res.data 52 | }) 53 | }) 54 | 55 | const updateValue = () => { 56 | return SuperOverviewApi.updateGlobalConfig('site', value.value) 57 | } 58 | return { 59 | value, 60 | updateValue, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth' 2 | export * from './user' 3 | export * from './form' 4 | export * from './ui' 5 | -------------------------------------------------------------------------------- /src/composables/ui.ts: -------------------------------------------------------------------------------- 1 | import { useWindowSize } from '@vueuse/core' 2 | import { computed } from 'vue' 3 | 4 | export function useIsMobile() { 5 | const { width } = useWindowSize() 6 | const isMobile = computed(() => width.value < 768) 7 | return isMobile 8 | } 9 | -------------------------------------------------------------------------------- /src/composables/user.ts: -------------------------------------------------------------------------------- 1 | import { computed, onMounted, reactive, ref } from 'vue' 2 | import { UserApi } from '@/apis' 3 | import { formatSize } from '@/utils/stringUtil' 4 | 5 | export function useSpaceUsage() { 6 | const usageData = reactive({ 7 | size: 0, 8 | usage: 0, 9 | limitUpload: false, 10 | wallet: '0:00', 11 | cost: '0.00', 12 | limitSpace: false, 13 | limitWallet: false, 14 | price: { 15 | storage: '0:00', 16 | download: '0:00', 17 | }, 18 | }) 19 | const usage = computed(() => usageData.usage) 20 | const size = computed(() => usageData.size) 21 | const percentage = computed(() => `${(usageData.usage / usageData.size * 100).toFixed(2)}%`) 22 | const walletPercentage = computed(() => `${(+usageData.cost / +usageData.wallet * 100).toFixed(2)}%`) 23 | const limitDownload = computed(() => usageData.limitUpload) 24 | const limitSpace = computed(() => usageData.limitSpace) 25 | const limitWallet = computed(() => usageData.limitWallet) 26 | const spaceUsageText = computed(() => { 27 | return `空间 ${percentage.value}: ${formatSize(usageData.usage)} / ${formatSize(usageData.size)}` 28 | }) 29 | const moneyUsageText = computed(() => { 30 | return `钱包 ${walletPercentage.value}: ${usageData.cost} / ${usageData.wallet}¥` 31 | }) 32 | const priceText = computed(() => { 33 | return `存储 ${usageData.price.storage}¥ + 下载 ${usageData.price.download}¥` 34 | }) 35 | 36 | onMounted(() => { 37 | UserApi.usage().then((res) => { 38 | Object.assign(usageData, res.data) 39 | }) 40 | }) 41 | return { 42 | usage, 43 | size, 44 | percentage, 45 | limitDownload, 46 | limitSpace, 47 | limitWallet, 48 | spaceUsageText, 49 | moneyUsageText, 50 | priceText, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 用户状态 3 | */ 4 | export enum USER_STATUS { 5 | /** 6 | * 正常 7 | */ 8 | NORMAL, 9 | /** 10 | * 冻结 11 | */ 12 | FREEZE, 13 | /** 14 | * 封禁 15 | */ 16 | BAN 17 | } 18 | 19 | export enum WishStatus { 20 | /** 21 | * 审核中 22 | */ 23 | REVIEW, 24 | /** 25 | * 待开始 26 | */ 27 | WAIT, 28 | /** 29 | * 开发中 30 | */ 31 | START, 32 | /** 33 | * 已上线 34 | */ 35 | END, 36 | /** 37 | * 关闭 38 | */ 39 | CLOSE 40 | } 41 | 42 | export enum ActionType { 43 | /** 44 | * 点赞 45 | */ 46 | PRAISE, 47 | 48 | /** 49 | * 文件下载 50 | */ 51 | Download, 52 | 53 | /** 54 | * 文件归档 55 | */ 56 | Compress, 57 | 58 | /** 59 | * 路由禁用 60 | */ 61 | DisabledRoute 62 | } 63 | 64 | export enum DownloadStatus { 65 | /** 66 | * 归档中 67 | */ 68 | ARCHIVE, 69 | /** 70 | * 链接已失效 71 | */ 72 | EXPIRED, 73 | /** 74 | * 可下载 75 | */ 76 | SUCCESS, 77 | /** 78 | * 归档失败 79 | */ 80 | FAIL 81 | } 82 | 83 | export const filenamePattern = /[\\/:*?"<>|]/g 84 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMetaEnv { 2 | VITE_APP_TITLE: string 3 | VITE_APP_AXIOS_BASE_URL: string 4 | // PV 上报路径 5 | VITE_APP_PV_PATH: string 6 | } 7 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import JsonViewer from 'vue-json-viewer' 3 | import router from './router' 4 | import store from './store' 5 | 6 | import App from './App.vue' 7 | import Axios from './apis/ajax' 8 | 9 | document.title = import.meta.env.VITE_APP_TITLE 10 | 11 | const app = createApp(App) 12 | 13 | app.provide('$http', Axios) 14 | 15 | app.use(router) 16 | app.use(store) 17 | app.use(JsonViewer) 18 | 19 | app.mount('#app') 20 | -------------------------------------------------------------------------------- /src/pages/404/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 29 | 60 | -------------------------------------------------------------------------------- /src/pages/about/index.vue: -------------------------------------------------------------------------------- 1 | 87 | 98 | 153 | -------------------------------------------------------------------------------- /src/pages/callme/index.vue: -------------------------------------------------------------------------------- 1 | 38 | 74 | 156 | -------------------------------------------------------------------------------- /src/pages/dashboard/manage/config/index.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 113 | 114 | 158 | -------------------------------------------------------------------------------- /src/pages/dashboard/manage/index.vue: -------------------------------------------------------------------------------- 1 | 41 | 63 | 64 | 106 | -------------------------------------------------------------------------------- /src/pages/dashboard/tasks/components/CategoryPanel.vue: -------------------------------------------------------------------------------- 1 | 61 | 144 | 209 | -------------------------------------------------------------------------------- /src/pages/dashboard/tasks/components/CreateTask.vue: -------------------------------------------------------------------------------- 1 | 33 | 80 | 94 | -------------------------------------------------------------------------------- /src/pages/dashboard/tasks/components/TaskInfo.vue: -------------------------------------------------------------------------------- 1 | 66 | 75 | 130 | -------------------------------------------------------------------------------- /src/pages/dashboard/tasks/components/infoPanel/ddl.vue: -------------------------------------------------------------------------------- 1 | 27 | 97 | -------------------------------------------------------------------------------- /src/pages/dashboard/tasks/components/infoPanel/file.vue: -------------------------------------------------------------------------------- 1 | 81 | 167 | 179 | -------------------------------------------------------------------------------- /src/pages/dashboard/tasks/components/infoPanel/template.vue: -------------------------------------------------------------------------------- 1 | 46 | 141 | 142 | 147 | -------------------------------------------------------------------------------- /src/pages/dashboard/tasks/components/infoPanel/tip.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | 30 | 57 | -------------------------------------------------------------------------------- /src/pages/dashboard/tasks/components/infoPanel/tipInfo.vue: -------------------------------------------------------------------------------- 1 | 2 | 145 | 146 | 203 | -------------------------------------------------------------------------------- /src/pages/dashboard/tasks/public.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage } from 'element-plus' 2 | import { TaskApi } from '@/apis' 3 | import { debounce } from '@/utils/other' 4 | 5 | export const updateTaskInfo: ( 6 | key: string, 7 | options: TaskApiTypes.TaskInfo, 8 | successInfo?: boolean 9 | ) => void = debounce( 10 | (key, options, successInfo = true) => { 11 | if (key) { 12 | TaskApi.updateTaskMoreInfo(key, options) 13 | .then(() => { 14 | if (successInfo) { 15 | ElMessage.success({ 16 | message: '设置成功', 17 | zIndex: 4000, 18 | duration: 1000 19 | }) 20 | } 21 | }) 22 | .catch(() => { 23 | ElMessage.error({ 24 | message: '设置失败', 25 | zIndex: 4000 26 | }) 27 | }) 28 | } 29 | }, 30 | 1000, 31 | true 32 | ) 33 | -------------------------------------------------------------------------------- /src/pages/disabled/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 16 | 48 | -------------------------------------------------------------------------------- /src/pages/feedback/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/pages/home/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 41 | 42 | 75 | -------------------------------------------------------------------------------- /src/pages/reset/index.vue: -------------------------------------------------------------------------------- 1 | 65 | 162 | 163 | 187 | -------------------------------------------------------------------------------- /src/pages/wish/index.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 131 | 132 | 171 | -------------------------------------------------------------------------------- /src/router/Interceptor/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'vue-router' 2 | import axios from 'axios' 3 | import $store from '@/store' 4 | import { PublicApi, SuperOverviewApi, UserApi } from '@/apis' 5 | 6 | declare module 'vue-router' { 7 | interface RouteMeta { 8 | // 是否管理员页面 9 | isAdmin?: boolean 10 | isSystem?: boolean 11 | // 是否需要登录 12 | requireLogin?: boolean 13 | // 路由title 14 | title?: string 15 | // 是否可以禁用 16 | allowDisabled?: boolean 17 | } 18 | } 19 | 20 | function registerRouteGuard(router: Router) { 21 | /** 22 | * 全局前置守卫 23 | */ 24 | router.beforeEach((to, from) => { 25 | // 上报PV 26 | const { fullPath } = to 27 | PublicApi.reportPv(fullPath) 28 | 29 | if (!import.meta.env.VITE_APP_PV_PATH.includes(window.location.hostname)) { 30 | axios.get( 31 | `//${ 32 | import.meta.env.VITE_APP_PV_PATH 33 | }/public/report/pv?path=${encodeURIComponent(window.location.href)}` 34 | ) 35 | } 36 | 37 | // 更改title 38 | window.document.title = `${import.meta.env.VITE_APP_TITLE} ${to.meta.title}` 39 | 40 | // if (to.meta.requireLogin) { 41 | // if (from.path === '/') { 42 | // return from 43 | // } 44 | // return false 45 | // } 46 | return true 47 | }) 48 | 49 | /** 50 | * 全局解析守卫 51 | */ 52 | router.beforeResolve(async (to) => { 53 | if (to.meta.isAdmin || to.meta.isSystem) { 54 | try { 55 | const powerData = (await UserApi.checkPower()).data 56 | $store.commit('user/setSuperAdmin', powerData.power) 57 | $store.commit('user/setSystem', powerData.system) 58 | if (to.meta.isSystem) { 59 | return ( 60 | powerData.system || { 61 | name: '404' 62 | } 63 | ) 64 | } 65 | 66 | if (to.meta.isAdmin) { 67 | return ( 68 | powerData.power || { 69 | name: '404' 70 | } 71 | ) 72 | } 73 | } catch (error) { 74 | // if (error instanceof NotAllowedError) { 75 | // // ... 处理错误,然后取消导航 76 | // return false 77 | // } else { 78 | // // 意料之外的错误,取消导航并把错误传给全局处理器 79 | // throw error 80 | // } 81 | console.error(error) 82 | return false 83 | } 84 | } 85 | return true 86 | }) 87 | 88 | /** 89 | * 全局后置守卫 90 | */ 91 | router.afterEach((to, from, failure) => { 92 | if (to.meta.allowDisabled) { 93 | SuperOverviewApi.checkDisabledRoute(to.path).then((v) => { 94 | if (v.data.status) { 95 | router.replace({ 96 | name: 'disable', 97 | query: { 98 | title: to.meta.title || to.path 99 | } 100 | }) 101 | } 102 | }) 103 | } 104 | // 改标题,监控上报一些基础信息 105 | // sendToAnalytics(to.fullPath) 106 | if (failure) { 107 | console.error(failure) 108 | } 109 | }) 110 | } 111 | 112 | export default registerRouteGuard 113 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import registerRouteGuard from './Interceptor' 3 | import routes from './routes' 4 | 5 | const router = createRouter({ 6 | history: createWebHistory(import.meta.env.VITE_ROUTER_BASE as string), 7 | routes 8 | }) 9 | 10 | // 注册路由守卫 11 | registerRouteGuard(router) 12 | 13 | export default router 14 | -------------------------------------------------------------------------------- /src/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | import Home from '@/pages/home/index.vue' 3 | import Login from '@/pages/login/index.vue' 4 | import Register from '@/pages/register/index.vue' 5 | // import Wish from '@/pages/wish/index.vue' 6 | 7 | const NotFind = () => import('@/pages/404/index.vue') 8 | const Reset = () => import('@/pages/reset/index.vue') 9 | const About = () => import('@/pages/about/index.vue') 10 | const Author = () => import('@/pages/callme/index.vue') 11 | const Feedback = () => import('@/pages/feedback/index.vue') 12 | const Dashboard = () => import('@/pages/dashboard/index.vue') 13 | const Files = () => import('@/pages/dashboard/files/index.vue') 14 | const Config = () => import('@/pages/dashboard/config/index.vue') 15 | const Tasks = () => import('@/pages/dashboard/tasks/index.vue') 16 | const Manage = () => import('@/pages/dashboard/manage/index.vue') 17 | const Overview = () => import('@/pages/dashboard/manage/overview/index.vue') 18 | const User = () => import('@/pages/dashboard/manage/user/index.vue') 19 | const Wish = () => import('@/pages/dashboard/manage/wish/index.vue') 20 | const Task = () => import('@/pages/task/index.vue') 21 | const Disabled = () => import('@/pages/disabled/index.vue') 22 | const DashboardConfig = () => 23 | import('@/pages/dashboard/manage/config/index.vue') 24 | 25 | const routes: RouteRecordRaw[] = [ 26 | // 404 27 | { 28 | path: '/:pathMatch(.*)*', 29 | name: '404', 30 | component: NotFind, 31 | meta: { 32 | title: '404' 33 | } 34 | }, 35 | { 36 | path: '/disabled', 37 | name: 'disable', 38 | component: Disabled 39 | }, 40 | { 41 | path: '/', 42 | name: 'home', 43 | component: Home, 44 | meta: { 45 | title: '首页', 46 | allowDisabled: true 47 | } 48 | }, 49 | // { 50 | // path: '/wish', 51 | // name: 'wish', 52 | // component: Wish, 53 | // meta: { 54 | // title: '需求墙', 55 | // }, 56 | // }, 57 | { 58 | path: '/login', 59 | name: 'login', 60 | component: Login, 61 | meta: { 62 | title: '登录' 63 | } 64 | }, 65 | { 66 | path: '/register', 67 | name: 'register', 68 | component: Register, 69 | meta: { 70 | title: '注册', 71 | allowDisabled: true 72 | } 73 | }, 74 | { 75 | path: '/reset', 76 | name: 'reset', 77 | component: Reset, 78 | meta: { 79 | title: '找回密码', 80 | allowDisabled: true 81 | } 82 | }, 83 | { 84 | path: '/about', 85 | name: 'about', 86 | component: About, 87 | meta: { 88 | title: '关于' 89 | } 90 | }, 91 | { 92 | path: '/author', 93 | name: 'author', 94 | component: Author, 95 | meta: { 96 | title: '联系作者' 97 | } 98 | }, 99 | { 100 | path: '/feedback', 101 | name: 'feedback', 102 | component: Feedback, 103 | meta: { 104 | title: '建议反馈' 105 | } 106 | }, 107 | { 108 | path: '/task/:key', 109 | name: 'task', 110 | component: Task, 111 | meta: { 112 | title: '文件提交' 113 | } 114 | }, 115 | { 116 | path: '/dashboard', 117 | name: 'dashboard', 118 | component: Dashboard, 119 | redirect: { 120 | name: 'tasks' 121 | }, 122 | children: [ 123 | { 124 | name: 'config', 125 | path: 'config', 126 | component: Config, 127 | meta: { 128 | title: '服务状态维护', 129 | isSystem: true 130 | } 131 | }, 132 | { 133 | name: 'files', 134 | path: 'files', 135 | component: Files, 136 | meta: { 137 | title: '文件列表' 138 | } 139 | }, 140 | { 141 | name: 'tasks', 142 | path: 'tasks', 143 | component: Tasks, 144 | meta: { 145 | title: '任务列表' 146 | } 147 | }, 148 | { 149 | name: 'manage', 150 | path: 'manage', 151 | component: Manage, 152 | redirect: { 153 | name: 'overview' 154 | }, 155 | children: [ 156 | { 157 | name: 'overview', 158 | path: 'overview', 159 | component: Overview, 160 | meta: { 161 | title: '应用概况', 162 | isAdmin: true 163 | } 164 | }, 165 | { 166 | name: 'user', 167 | path: 'user', 168 | component: User, 169 | meta: { 170 | title: '用户列表', 171 | isAdmin: true 172 | } 173 | }, 174 | { 175 | name: 'wish', 176 | path: 'wish', 177 | component: Wish, 178 | meta: { 179 | title: '需求管理', 180 | isAdmin: true 181 | } 182 | }, 183 | { 184 | name: 'dashboard-config', 185 | path: 'config', 186 | component: DashboardConfig, 187 | meta: { 188 | title: '配置面板', 189 | isAdmin: true 190 | } 191 | } 192 | ] 193 | } 194 | ] 195 | } 196 | ] 197 | export default routes 198 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue' 3 | 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | import user from './modules/user' 3 | import category from './modules/category' 4 | import task from './modules/task' 5 | 6 | // Create a new store instance. 7 | const store = createStore({ 8 | modules: { 9 | user, 10 | category, 11 | task, 12 | }, 13 | }) 14 | 15 | export default store 16 | -------------------------------------------------------------------------------- /src/store/modules/category.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'vuex' 2 | import { CategoryApi } from '@/apis' 3 | 4 | interface State { 5 | categoryList: any[] 6 | } 7 | 8 | const store: Module = { 9 | namespaced: true, 10 | state() { 11 | return { 12 | categoryList: [] 13 | } 14 | }, 15 | mutations: { 16 | updateCategory(state, payload) { 17 | state.categoryList = payload 18 | } 19 | }, 20 | actions: { 21 | getCategory(context) { 22 | CategoryApi.getList().then((res) => { 23 | context.commit('updateCategory', res.data.categories) 24 | }) 25 | }, 26 | createCategory(context, name) { 27 | return CategoryApi.createNew(name).then((res) => { 28 | context.dispatch('getCategory') 29 | return res 30 | }) 31 | }, 32 | deleteCategory(context, k) { 33 | return CategoryApi.deleteOne(k).then((res) => { 34 | const idx = context.state.categoryList.findIndex((v) => v.k === k) 35 | if (idx >= 0) { 36 | context.state.categoryList.splice(idx, 1) 37 | } 38 | return res 39 | }) 40 | } 41 | } 42 | } 43 | 44 | export default store 45 | -------------------------------------------------------------------------------- /src/store/modules/task.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'vuex' 2 | import { TaskApi } from '@/apis' 3 | 4 | interface State { 5 | taskList: any[] 6 | } 7 | 8 | const store: Module = { 9 | namespaced: true, 10 | state() { 11 | return { 12 | taskList: [] 13 | } 14 | }, 15 | mutations: { 16 | updateTask(state, payload) { 17 | state.taskList = payload 18 | } 19 | }, 20 | actions: { 21 | getTask(context) { 22 | TaskApi.getList().then((res) => { 23 | context.commit('updateTask', res.data.tasks) 24 | }) 25 | }, 26 | createTask(context, payload) { 27 | const { name, category } = payload 28 | return TaskApi.create(name, category).then((res) => { 29 | context.dispatch('getTask') 30 | return res 31 | }) 32 | }, 33 | deleteTask(context, k) { 34 | return TaskApi.deleteOne(k).then((res) => { 35 | const idx = context.state.taskList.findIndex((v) => v.key === k) 36 | const targetTask = context.state.taskList[idx] 37 | if (targetTask && targetTask.category === 'trash') { 38 | context.state.taskList.splice(idx, 1) 39 | } else { 40 | targetTask.category = 'trash' 41 | } 42 | return res 43 | }) 44 | }, 45 | updateTask(context, payload) { 46 | const { key, name, category } = payload 47 | return TaskApi.updateBaseInfo(key, name, category).then((res) => { 48 | context.dispatch('getTask') 49 | return res 50 | }) 51 | } 52 | } 53 | } 54 | 55 | export default store 56 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'vuex' 2 | import { UserApi } from '@/apis' 3 | 4 | interface State { 5 | token: string 6 | isSuperAdmin: boolean 7 | isLogin: boolean 8 | system: boolean 9 | } 10 | 11 | const store: Module = { 12 | namespaced: true, 13 | state() { 14 | return { 15 | token: localStorage.getItem('token') as string, 16 | isSuperAdmin: false, 17 | isLogin: false, 18 | system: localStorage.getItem('token') === 'true' 19 | } 20 | }, 21 | // 只能同步 22 | mutations: { 23 | setToken(state, payload) { 24 | state.token = payload 25 | if (payload) { 26 | localStorage.setItem('token', payload) 27 | } else { 28 | localStorage.removeItem('token') 29 | } 30 | }, 31 | setSystem(state, payload) { 32 | state.system = payload 33 | if (payload) { 34 | localStorage.setItem('system', payload) 35 | } else { 36 | localStorage.removeItem('system') 37 | } 38 | }, 39 | setSuperAdmin(state, payload) { 40 | state.isSuperAdmin = payload 41 | }, 42 | setLoginStatue(state, payload) { 43 | state.isLogin = payload?.isLogin 44 | } 45 | }, 46 | actions: { 47 | getLoginStatus(context) { 48 | UserApi.checkLoginStatus().then((res) => { 49 | context.commit('setLoginStatue', { 50 | isLogin: res.data 51 | }) 52 | }) 53 | } 54 | } 55 | } 56 | 57 | export default store 58 | -------------------------------------------------------------------------------- /src/utils/elementUI.ts: -------------------------------------------------------------------------------- 1 | import { App } from '@vue/runtime-core' 2 | import ElementPlus from 'element-plus' 3 | import 'element-plus/dist/index.css' 4 | import zhCn from 'element-plus/es/locale/lang/zh-cn' 5 | 6 | export default function mountElementUI(app: App) { 7 | app.use(ElementPlus, { locale: zhCn }) 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/other.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-rest-params */ 2 | /* eslint-disable @typescript-eslint/no-this-alias */ 3 | export function debounce(func, wait = 1000, immediate = false) { 4 | let timeout 5 | let count = 0 6 | return function () { 7 | count += 1 8 | const context = this 9 | const args = arguments 10 | const later = function () { 11 | timeout = null 12 | if (count > 0) { 13 | func.apply(context, args) 14 | count = 0 15 | } 16 | } 17 | const callNow = immediate && !timeout 18 | clearTimeout(timeout) 19 | timeout = setTimeout(later, wait) 20 | if (callNow) { 21 | func.apply(context, args) 22 | count = 0 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/regExp.ts: -------------------------------------------------------------------------------- 1 | // 手机号 2 | export const rMobilePhone = /^1\d{10}$/ 3 | 4 | // 账号 5 | export const rAccount = /^(\d|[a-zA-Z]){4,11}$/ 6 | 7 | // 密码 支持字母/数字/下划线(6-16) 8 | export const rPassword = /^\w{6,16}$/ 9 | 10 | // 验证码 11 | export const rVerCode = /^\d{4}$/ 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "jsx": "preserve", 5 | "lib": ["esnext", "dom"], 6 | "baseUrl": "./", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "paths": { 10 | "@/*": [ 11 | "src/*" 12 | ], 13 | "@components/": ["src/components/*"] 14 | }, 15 | "resolveJsonModule": true, 16 | "types": ["vite/client", "node", "element-plus/global"], 17 | "allowJs": true, 18 | "strict": false, 19 | "sourceMap": true, 20 | "esModuleInterop": true 21 | }, 22 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "docs/**/*.ts", "docs/**/*.d.ts", "docs/vite.config.mts"], 23 | "exclude": ["vite.config.mts"] 24 | } 25 | -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import AutoImport from 'unplugin-auto-import/vite' 5 | import Components from 'unplugin-vue-components/vite' 6 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 7 | import legacy from '@vitejs/plugin-legacy' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | plugins: [ 12 | legacy({ 13 | targets: ['defaults', 'not IE 11'], 14 | }), 15 | vue(), 16 | AutoImport({ 17 | resolvers: [ElementPlusResolver()], 18 | }), 19 | Components({ 20 | resolvers: [ElementPlusResolver()], 21 | }), 22 | // ElementPlus({ 23 | // defaultLocale: 'zh-cn' 24 | // }) 25 | ], 26 | optimizeDeps: { 27 | include: ['vue', 'vue-router', 'vuex', 'axios', 'vue-json-viewer'], 28 | }, 29 | build: { 30 | sourcemap: true, 31 | }, 32 | server: { 33 | host: '0.0.0.0', 34 | proxy: { 35 | '/api/': { 36 | target: 'http://127.0.0.1:3000', 37 | changeOrigin: true, 38 | rewrite: p => p.replace(/^\/api/, ''), 39 | }, 40 | '/api-test/': { 41 | target: 'https://ep.test.sugarat.top', 42 | changeOrigin: true, 43 | rewrite: p => p.replace(/^\/api-test/, 'api/'), 44 | }, 45 | }, 46 | }, 47 | resolve: { 48 | alias: { 49 | '@': path.resolve(__dirname, './src'), 50 | '@components': path.resolve(__dirname, './src/components'), 51 | }, 52 | }, 53 | }) 54 | --------------------------------------------------------------------------------