├── .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 |
15 |
16 |
17 |
18 | 
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 | 
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 | 
42 |
43 | 可以打开浏览器访问一下 http://localhost:6478 即可看到页面
44 |
45 | ③ 获取系统账号,登录后台
46 |
47 | ```sh
48 | docker logs easypicker2
49 | ```
50 |
51 | 
52 |
53 | 访问 http://localhost:6478/login ,输入账号密码即可登录后台
54 |
55 | 
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 | 
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 | 
215 |
216 | 4 创建网站
217 |
218 | 在网站的 nginx 配置文件中添加`/`路由,通过反向代理将流量转发至 docker 启动的容器
219 |
220 | 
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 | 
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 | 
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 | 
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 | 
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 | 
85 |
86 | 
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 | 
23 |
24 | 新建空间,输入一些必要的数据
25 |
26 | 
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 | 
48 |
49 | 如果需要长期使用,建议绑定一个自定义域名,
50 |
51 | :::tip 我没有域名怎么办
52 | 当然如果你没有域名,可以[联系作者](../author.md),提供一个`.sugarat.top`下的3级,4级域名
53 | :::
54 |
55 | ## 3. 获取到域名
56 |
57 | 进入我们创建的空间`easypicker-test`,就能看到提供的测试域名
58 |
59 | 
60 |
61 | 如果需要添加自己的域名,请在下面的页面自定义源站域名绑定自己的域名映射到空间内
62 |
63 | 
64 |
65 | ## 4. 获取ack与sek
66 |
67 | :::warning 重要提示!!!-
68 | **这两个东西千万不要泄露!!!**
69 |
70 | **这两个东西千万不要泄露!!!**
71 | :::
72 |
73 | 当然泄漏了可自己进行重置
74 |
75 | 获取位置如下
76 |
77 | 控制面板右上角的秘钥管理
78 |
79 | 
80 |
81 | 接下来就能看到
82 |
83 | 
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 | 
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 | 
125 |
126 | 在打开的详情页面中找到 `HTTP响应头配置`
127 |
128 | 
129 |
130 | 添加一条规则,然后点击确定即可
131 |
132 | ```sh
133 | Content-Disposition attachment
134 | ```
135 |
136 | 
137 |
138 | ## 7. 设置图片样式(可选)
139 |
140 | 现在手机拍摄的图片往往都很大,动辄10几兆,为了加快图片的预览与节省服务带宽可以配置七牛云的图片样式进行裁剪
141 |
142 | 
143 |
144 | 点击新建图片样式,然后根据指引操作,完成创建
145 |
146 | 共需要两个样式,一个缩略图一个预览图,下面是场景示例
147 |
148 | | 缩略图 | 预览图 |
149 | | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- |
150 | |  |  |
151 |
152 | 设置样式分隔符
153 |
154 | 
155 |
156 | 在配置面板中更新即可
157 |
158 | - **注意**
159 | - 不同存储空间之间的样式不互通
160 | - 填入格式是分隔符+样式名
161 |
162 | 完成配置后重启服务即可
163 | :::details 如果应用版本 < v2.1.9,需要手动在配置文件中更新
164 |
165 | 将创建好的样式名和分隔符,填入到服务端的环境变量中
166 |
167 | 
168 | :::
169 |
170 | ## 8. 绑定自定义域名(可选)
171 |
172 | 在存储空间里找到`域名管理`,点击绑定域名即可
173 |
174 | 
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 | 
184 |
185 | 填写完成后点击`创建`即可,然后按照要求添加域名解析
186 |
187 | 可以自行阅读[七牛云提供的域名绑定文档](https://developer.qiniu.com/kodo/8527/kodo-domain-name-management)完成
188 |
189 | 有其它问题可以小群交流,方便可以加入及时交流沟通问题: 685446473
190 |
191 | 
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 | 
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 | 
13 |
14 | 禁用对应页面将无法访问,会有如下提示。
15 |
16 | 
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 | |
|
|
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 | |  |  |  |
39 |
40 | ## 爱发电
41 |
42 | **爱发电主页:https://afdian.com/a/sugarat**
43 |
44 | 
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 |
2 |
3 |

4 |
7 |
8 | 前端切图仔
9 |
热爱开源,乐于分享
10 |
11 |
19 |
20 |
21 |
70 |
--------------------------------------------------------------------------------
/docs/src/components/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
微信赞赏
6 |
10 |
11 |
12 | 如果你觉得项目不错
13 |
14 |
15 | 可以
16 | 请作者喝 🍵
19 |
20 |
21 |
22 | -
23 |
Developer
24 |
27 |
28 | -
29 |
30 | 反馈交流群
33 |
34 |
35 |
39 |
40 |
41 |
42 |
43 |
47 |
48 |
92 |
--------------------------------------------------------------------------------
/docs/src/components/Picture.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
--------------------------------------------------------------------------------
/docs/src/components/Praise.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ v.title }} |
7 |
8 |
9 |
10 |
11 |
12 |
21 | |
22 |
23 |
24 |
25 |
26 |
27 |
46 |
55 |
--------------------------------------------------------------------------------
/docs/src/components/WishBtn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 提建议 & 给反馈
12 | 取消
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
34 |
35 |
36 |
40 |
41 |
42 |
43 | 提交
44 |
45 |
46 |
47 |
48 |
91 |
92 |
99 |
--------------------------------------------------------------------------------
/docs/src/components/WishPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 规划中
7 |
8 |
9 |
10 | 开发中
11 |
12 |
13 |
14 | 已上线
15 |
16 |
17 |
18 | -
19 |
20 |
21 |
22 |
23 |
24 | {{ d.title }}
25 |
26 |
27 | {{ d.des }}
28 |
29 |
30 |
31 |
32 |
33 |
34 | {{ d.count }}票
35 |
36 |
37 |
38 |
39 |
40 |
41 | 💐 已处理完毕的反馈归档 💐
42 | -
43 |
44 |
45 |
46 |
47 |
48 | {{ d.title }}
49 |
50 |
51 | {{ d.des }}
52 |
53 |
54 |
55 |
56 |
57 |
58 | {{ d.count }}票
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
115 |
171 |
--------------------------------------------------------------------------------
/docs/src/components/callme/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | -
7 |
{{ qrcode.text }}
8 |
9 |
![]()
10 |
11 |
12 |
13 |
14 |
15 |
如遇无法解决的账号/使用问题,欢迎小窗联系我
16 |
17 |
18 |
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 |
12 |
13 |
14 |
15 | ☕️ 支持一下作者 👉🏻
16 |
17 | 爱发电
18 |
19 | |
20 |
21 | 赞赏
22 |
23 |
24 |
25 |
26 |
27 |
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 |
2 |
26 |
27 |
28 |
96 |
97 |
135 |
--------------------------------------------------------------------------------
/src/components/HomeHeader/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
93 |
156 |
--------------------------------------------------------------------------------
/src/components/InfosForm/index.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
23 |
24 | {{ info.text }}
25 |
26 |
34 |
35 |
40 | {{ r.text }}
41 |
42 |
43 |
51 |
57 |
58 |
59 |
60 |
61 |
62 |
81 |
--------------------------------------------------------------------------------
/src/components/MessageList/index.vue:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 |
56 | 暂无更多消息 ღ( ´・ᴗ・` )比心
57 |
58 |
75 |
85 |
86 |
87 |
88 | 时间:{{ formatDate(new Date(activeMessage.date)) }}
89 |
90 |
91 | {
95 | dialogMessage.show = false
96 | }
97 | "
98 | >
99 | 下次提醒
100 |
101 | 确定
102 |
103 |
104 |
105 |
106 |
107 |
151 |
152 |
162 |
--------------------------------------------------------------------------------
/src/components/MessagePanel/index.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
47 |
--------------------------------------------------------------------------------
/src/components/Praise/index.vue:
--------------------------------------------------------------------------------
1 |
48 |
49 |
50 |
51 |
52 |
53 |
59 |
60 | 目前的服务开销主要在 “文件存储” 与 "资源下载"两方面
61 |
62 | 存储 0.15 元/GB/月,下载0.29 元/GB
63 | 如果你觉得应用不错,可以支持一下👍🏻
64 |
65 |
66 |
67 |
68 | {{ v.title }} |
69 |
70 |
71 |
72 |
73 |
74 |
82 | |
83 |
84 |
85 |
86 |
87 |
88 |
92 |
93 |
94 |
95 |
96 |
97 |
120 |
--------------------------------------------------------------------------------
/src/components/QrCode.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
45 |
--------------------------------------------------------------------------------
/src/components/linkDialog.vue:
--------------------------------------------------------------------------------
1 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | 复制
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | 生成短链
108 |
109 |
110 |
111 |
112 |
116 |
117 |
118 |
119 | 复制分享信息
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
133 |
--------------------------------------------------------------------------------
/src/components/loginPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
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 |
2 |
3 |
404 Not Found
4 |
糟糕页面走丢了
5 |
{{ time }}s后回到首页
6 |
7 |
8 |
29 |
60 |
--------------------------------------------------------------------------------
/src/pages/about/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
简介
11 |
在线文件收取助手,帮助用户快速的完成一对多的文件收取
12 |
13 |
14 |
诞生背景
15 |
校园学习或者工作场景中会有以下几个场景:
16 |
17 | - 电子文件: 班委向同学收取各种实验电子报告
18 | - 图片: 收取各种截图证明/活动照片
19 | - ...
20 |
21 |
目前最广泛的收取方式为,通过邮箱,QQ,微信等通讯工具传递
22 |
弊端显而易见,不方便整理统计。还占用电脑/手机内存
23 |
为了解决这个问题,此项目应运而生
24 |
当然市面上也有几款类似的办公工具 ,功能及使用方式都差不多
25 |
本项目是 开源 的
26 |
如有私有化的需求,可自行部署使用(也可 联系作者 提供帮助)
27 |
28 |
29 |
现有功能 (用户关心的)
30 |
收集任务
31 |
32 | - 分类管理: 收集任务支持分类管理
33 | - 设置DDL(截止日期): 任务截止后不可再提交文件
34 | - 限制提交人员: 非指定的人员(姓名),无法提交文件
35 | - 设置模板文件: 提交者可直接在提交页面下载此文件
36 | - 自动重命名: 提交的文件按填写表单信息进行自动重名(如:姓名-工号.后缀)
37 |
38 |
文件相关
39 |
40 | - 上传(提交): 单个/多个
41 | - 下载: 单个/多个/按收集任务
42 | - 撤回: 用户可以撤回自己提交的文件
43 | - 分享: 用户可以将文件下载链接分享给朋友
44 | - 模板下载: 下载任务发布者设置的模板文件
45 | - 导出: 导出提交记录
46 |
47 |
48 |
49 |
相关链接
50 |
应用链接相关
51 |
56 |
应用源代码 (开发者需要的)
57 |
61 |
65 |
69 |
作者 (夹带私货)
70 |
75 |
76 |
84 |
85 |
86 |
87 |
98 |
153 |
--------------------------------------------------------------------------------
/src/pages/callme/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
联系作者
6 |
7 |
16 |
17 |
18 |
19 |
20 | -
21 |
{{ qrcode.text }}
22 |
23 |
![]()
24 |
25 |
26 |
27 |
28 |
29 |
如遇无法解决的账号/使用问题,欢迎小窗联系我
30 |
31 |
32 | 回到首页
33 |
34 |
35 |
36 |
37 |
38 |
74 |
156 |
--------------------------------------------------------------------------------
/src/pages/dashboard/manage/config/index.vue:
--------------------------------------------------------------------------------
1 |
62 |
63 |
64 |
65 |
66 |
禁用路由管理
67 |
68 | -
69 |
75 | {{ r.title }}
76 | {{ r.path }}{{ r.path === '/register' ? ' 关闭后将同时禁用注册功能' : '' }}
77 |
78 |
79 |
全局配置管理(JSON)
80 |
81 |
82 | 更新
83 |
84 |
85 |
86 | 取消
87 |
88 |
89 | 保存
90 |
91 |
92 |
93 |
94 |
102 |
103 |
109 |
110 |
111 |
112 |
113 |
114 |
158 |
--------------------------------------------------------------------------------
/src/pages/dashboard/manage/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
63 |
64 |
106 |
--------------------------------------------------------------------------------
/src/pages/dashboard/tasks/components/CategoryPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
分类列表
7 | (点击分类可筛选任务)
8 |
9 |
10 |
11 |
12 |
13 |
22 | + New 分类
29 | 默认{{ taskCount('default') }}
34 | 回收站{{
40 | taskCount('trash')
41 | }}
44 |
45 |
46 |
47 | {{ tag.name }}{{ taskCount(tag.k) }}
56 |
57 |
58 |
59 |
60 |
61 |
144 |
209 |
--------------------------------------------------------------------------------
/src/pages/dashboard/tasks/components/CreateTask.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 | {{
9 | isShowCreateTask
10 | ? '关闭创建面板'
11 | : '创建收集任务'
12 | }}
13 |
14 |
15 |
16 |
17 |
18 |
19 | 确定
20 |
21 |
22 |
23 | 点击
24 |
25 |
26 | 可以进一步的调整任务
27 |
28 |
29 | 设置截止时间,自动重命名,名单限制,批注,文件模板。。🚀
30 |
31 |
32 |
33 |
80 |
94 |
--------------------------------------------------------------------------------
/src/pages/dashboard/tasks/components/TaskInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
40 |
41 |
42 |
43 |
44 |
45 | 暂时没有提交记录...
46 |
47 |
48 | -
49 | 近 {{ item.recentLog.length }} 条提交记录
50 | 查看详情
53 |
54 | -
59 | {{ formatDate(new Date(log.date)) }}
60 | {{ log.filename }}
61 |
62 |
63 |
64 |
65 |
66 |
75 |
130 |
--------------------------------------------------------------------------------
/src/pages/dashboard/tasks/components/infoPanel/ddl.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
设置截止日期,截止后将不能再提交文件。
11 |
12 |
20 | 取消
21 |
22 |
23 | {{ isOver ? '已经截止' : `剩余时间: ${waitTimeStr}` }}
24 |
25 |
26 |
27 |
97 |
--------------------------------------------------------------------------------
/src/pages/dashboard/tasks/components/infoPanel/file.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
↓下方设置允许提交的文件类型↓
4 |
暂时只支持通过文件名后缀进行卡控,不区分大小写
5 |
例如:txt,png,jpeg,webp
6 |
7 |
14 |
15 |
16 |
支持英文逗号","分割,一次添加多个
17 |
18 |
19 |
20 | 确定
21 |
22 |
23 |
24 |
32 | {{ tag }}
33 |
34 |
已添加: {{ formatData.format.join(',') }}
36 | 一键复制
39 |
40 |
41 |
42 |
↓下方设置最大同时提交文件数量(16 >= x >=1 默认 10)↓
45 |
46 |
52 |
53 |
54 |
↓下方设置文件最大的大小↓
55 |
1024B = 1KB, 1024KB = 1MB, 1024MB = 1GB
56 |
0表示不限制
57 |
58 |
64 |
70 |
71 |
72 |
73 |
74 |
{{
75 | formatData.size === 0
76 | ? '不限制大小'
77 | : `限制为不超过: ${formatSize(formatData.size)}`
78 | }}
79 |
80 |
81 |
167 |
179 |
--------------------------------------------------------------------------------
/src/pages/dashboard/tasks/components/infoPanel/template.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
设置的模板文件,可供用户在提交页下载。
9 |
删除
18 |
{{ template || '尚未设置模板文件' }}
19 |
20 |
29 |
30 | 选取文件
31 |
32 | 设为模板
39 |
40 | 选择模板文件,然后点击上传
41 |
42 |
43 |
44 |
45 |
46 |
141 |
142 |
147 |
--------------------------------------------------------------------------------
/src/pages/dashboard/tasks/components/infoPanel/tip.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
57 |
--------------------------------------------------------------------------------
/src/pages/dashboard/tasks/components/infoPanel/tipInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
145 |
146 |
147 |
148 |
153 | 设置注意事项,供用户提交时查看
154 |
155 |
注意控制字数和换行,避免展示异常,设置完记得预览一下,再投放
156 |
157 |
166 |
167 |
168 |
169 | 保存
170 |
171 |
172 | 清空
173 |
174 |
175 |
176 | 有变动记得保存
177 |
178 |
可以设置图片啦↓ 最多3张
179 |
191 |
192 |
193 |
201 |
202 |
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 |
2 |
3 |
EasyPicker | 轻取
4 |
{{ $route.query.title }}:已被网站管理员禁用
5 |
6 |
7 |
8 |
16 |
48 |
--------------------------------------------------------------------------------
/src/pages/feedback/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
反馈页
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/pages/home/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
EasyPicker
8 |
9 |
10 |
13 |
14 |
15 | 点我登陆
16 |
17 |
18 | 没有账号? 去注册
19 |
20 |
21 |
25 |
26 |
27 |
41 |
42 |
75 |
--------------------------------------------------------------------------------
/src/pages/reset/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
64 |
65 |
162 |
163 |
187 |
--------------------------------------------------------------------------------
/src/pages/wish/index.vue:
--------------------------------------------------------------------------------
1 |
62 |
63 |
64 |
65 |
66 |
69 |
70 |
71 | 需求墙
72 |
73 |
74 | 通过投票决定下一个新功能是什么
75 |
76 |
77 | 票数越多优先级越高
78 |
79 |
80 | 当然你也可以提出你的需求,让大家来投票
81 |
82 |
83 |
84 | Go Go Go!
85 | 我要提需求
86 |
87 |
88 | 提需求&投票,请先
89 |
90 | 登录
91 |
92 |
93 |
94 |
95 | 目前需求面板还未开发完成,
96 |
97 |
98 | 可先直接加群进行反馈
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
123 |
124 |
125 |
129 |
130 |
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 |
--------------------------------------------------------------------------------