<repo_name>gongsir0630/easypicker2-client<file_sep>/src/utils/regExp.ts
// 手机号
export const rMobilePhone = /^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|16[0-9]|17[0-9]|18[0|1|2|3|5|6|7|8|9])\d{8}$/

// 账号
export const rAccount = /^(\d|[a-zA-Z]){4,8}$/

// 密码 支持字母/数字/下划线(6-16)
export const rPassword = /^\w{6,16}$/

// 验证码
export const rVerCode = /^\d{4}$/
<file_sep>/src/apis/modules/file.ts
import ajax from '../ajax'

function getUploadToken() {
  return ajax.get<any, BaseResponse>('file/token')
}

function addFile(options:DbFile) {
  return ajax.post<any, BaseResponse>('file/info', options)
}

function getFileList() {
  return ajax.get<any, BaseResponse>('file/list')
}

function getTemplateUrl(template:string, key:string) {
  return ajax.get<any, BaseResponse>('file/template', {
    params: {
      template,
      key,
    },
  })
}

function getOneFileUrl(id:number) {
  return ajax.get<any, BaseResponse>('file/one', {
    params: {
      id,
    },
  })
}

function deleteOneFile(id:number) {
  return ajax.delete<any, BaseResponse>('file/one', {
    params: {
      id,
    },
  })
}

function batchDownload(ids:number[]) {
  return ajax.post<any, BaseResponse>('file/batch/down', {
    ids,
  })
}

function batchDel(ids:number[]) {
  return ajax.delete<any, BaseResponse>('file/batch/del', {
    params: {
      ids,
    },
  })
}

function checkCompressStatus(id:string) {
  return ajax.post<any, BaseResponse>('file/compress/status', {
    id,
  })
}
function getCompressDownUrl(key:string) {
  return ajax.post<any, BaseResponse>('file/compress/down', {
    key,
  })
}
function getCompressFileUrl(id:string):Promise<string> {
  const check = (_r:any) => {
    checkCompressStatus(id).then((r) => {
      const { code, key } = r.data
      if (code === 0) {
        getCompressDownUrl(key).then((v) => {
          const { url } = v.data
          _r(url)
        })
      } else {
        setTimeout(() => {
          check(_r)
        }, 1000)
      }
    })
  }

  return new Promise((resolve, reject) => {
    check(resolve)
  })
}
interface WithdrawFileOptions{
  taskKey:string
  taskName:string
  filename:string
  hash:string
  peopleName:string
  info:string
}
function withdrawFile(options:WithdrawFileOptions) {
  return ajax.delete('file/withdraw', {
    params: options,
  })
}
export default {
  getUploadToken,
  addFile,
  getFileList,
  getTemplateUrl,
  withdrawFile,
  getOneFileUrl,
  deleteOneFile,
  batchDownload,
  batchDel,
  checkCompressStatus,
  getCompressFileUrl,
  getCompressDownUrl,
}
<file_sep>/src/utils/networkUtil.ts
import { base64 } from './stringUtil'

/* eslint-disable no-restricted-syntax */
type SuccessCallback = (data:any) => void
export function jsonp(url: string, jsonpCallback: string, success:SuccessCallback) {
  const $script = document.createElement('script')
  $script.src = `${url}&callback=${jsonpCallback}`
  $script.async = true
  $script.type = 'text/javascript';
  (<any>window)[jsonpCallback] = function callback(data:any) {
    if (success) {
      success(data)
    }
  }
  document.body.appendChild($script)
}

interface UploadFileOptions{
  success?:any,
  error?:any,
  process?:any
  method?:string,
}
export function uploadFile(file:File, url:string, options?:UploadFileOptions) {
  const form = new FormData()
  // ajax对象
  const xhr = new XMLHttpRequest()
  // 添加文件到表单中
  form.append('file', file)
  // 设置请求方式 路径  是否异步
  xhr.open(options?.method || 'post', url, true)
  // 设置请求头参数的方式,如果没有可忽略此行代码
  xhr.setRequestHeader('token', localStorage.getItem('token') as string)
  if (options?.success) {
  // 上传完成
    xhr.onload = (e) => {
      const target = e?.currentTarget as any
      if (target.response) {
        options.success(JSON.parse(target.response))
        return
      }
      options.success()
    }
  }
  if (options?.error) {
    // 上传出错
    xhr.onerror = options.error
  }
  if (options?.process) {
  // 上传中
    xhr.onprogress = (e) => {
      const { total, loaded, lengthComputable } = e
      if (lengthComputable) {
        options.process((loaded / total).toFixed(2))
      }
    }
  }

  // 发送请求
  xhr.send(form)
}

/**
  * 导出表格数据为xls
  * @param headers 头部
  * @param body 身体部分数据
  */
export function tableToExcel(headers:string[], body:any[], filename = 'res.xls') {
  // 列标题
  let str = `<tr>${headers.map((v) => `<th>${v}</th>`).join('')}</tr>`
  // 循环遍历，每行加入tr标签，每个单元格加td标签
  for (const row of body) {
    str += '<tr>'
    for (const cell of row) {
      // 增加\t为了不让表格显示科学计数法或者其他格式
      str += `<td>${`${cell}\t`}</td>`
    }
    str += '</tr>'
  }

  // Worksheet名
  const worksheet = 'sheet1'
  const uri = 'data:application/vnd.ms-excel;base64,'

  // 下载的表格模板数据
  const template = '<html xmlns:o="urn:schemas-microsoft-com:office:office" \n'
          + '      xmlns:x="urn:schemas-microsoft-com:office:excel" \n'
          + '      xmlns="http://www.w3.org/TR/REC-html40">\n'
          + '      <head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet>\n'
          + `        <x:Name>${worksheet}</x:Name>\n`
          + '        <x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet>\n'
          + '        </x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]-->\n'
          + `        </head><body><table>${str}</table></body></html>\n`
  // 下载模板
  const tempA = document.createElement('a')
  tempA.href = uri + base64(template)
  tempA.download = filename
  document.body.appendChild(tempA)
  tempA.click()
  document.body.removeChild(tempA)
}

/**
 * 七牛云上传
 */
export function qiniuUpload(token:string, file:File, key:string, options?:UploadFileOptions) {
  const observable = qiniu.upload(file, key, token)
  const { success, error, process } = options || {}
  const subscription = observable.subscribe({
    next(res) {
      const { total: { percent } } = res
      if (process) {
        process(percent.toFixed(2), res, subscription)
      }
    },
    error(err) {
      if (error) {
        error(err, subscription)
      }
    },
    complete(res) {
      if (success) {
        success(res, subscription)
      }
    },
  })
  // subscription.close() // 取消上传
}

export function downLoadByUrl(url: string, filename = `${Date.now()}`) {
  const a = document.createElement('a')
  a.href = url
  a.target = '_blank'
  a.download = filename
  a.click()
}
<file_sep>/src/apis/modules/public.ts
import ajax from '../ajax'

/**
 * 获取验证码
 * @param mobile 手机号
 */
function getCode(phone: string) {
  return ajax.get<any, BaseResponse>(
    'public/code',
    {
      params: {
        phone,
      },
    },
  )
}

function reportPv(path:string) {
  return ajax.post<any, BaseResponse>(
    'public/report/pv',
    {
      path,
    },
  )
}
export default {
  getCode,
  reportPv,
}
<file_sep>/src/pages/dashboard/tasks/public.ts
import { TaskApi } from '@/apis'
import { ElMessage } from 'element-plus'

export const updateTaskInfo = (key:string, options: any) => {
  if (key) {
    TaskApi
      .updateTaskMoreInfo(key, options)
      .then(() => {
        ElMessage.success('设置成功')
      })
      .catch(() => {
        ElMessage.error('设置失败')
      })
  }
}
<file_sep>/build/compress.sh
# /bin/bash
compressDir="./dist" # 需要压缩目录 
compressFile="ep2.tar.gz"        # 压缩后的文件名
echo "开始-----归档解压"
tar -zvcf ${compressFile} ${compressDir}<file_sep>/src/router/routes/index.ts
import { RouteRecordRaw } from 'vue-router'
import Home from '@/pages/home/index.vue'

const NotFind = () => import('@/pages/404/index.vue')
const Login = () => import('@/pages/login/index.vue')
const Reset = () => import('@/pages/reset/index.vue')
const Register = () => import('@/pages/register/index.vue')
const About = () => import('@/pages/about/index.vue')
const Author = () => import('@/pages/callme/index.vue')
const Feedback = () => import('@/pages/feedback/index.vue')
const Dashboard = () => import('@/pages/dashboard/index.vue')
const Files = () => import('@/pages/dashboard/files/index.vue')
const Tasks = () => import('@/pages/dashboard/tasks/index.vue')
// const Manage = () => import('@/pages/dashboard/manage/index.vue')
// const Overview = () => import('@/pages/dashboard/manage/overview/index.vue')
// const User = () => import('@/pages/dashboard/manage/user/index.vue')
const Task = () => import('@/pages/task/index.vue')
const requireLogin = {
  requireLogin: false,
}
const routes: RouteRecordRaw[] = [
  // 404
  { path: '/:pathMatch(.*)*', name: '404', component: NotFind },
  { path: '/', name: 'home', component: Home },
  { path: '/login', name: 'login', component: Login },
  { path: '/register', name: 'register', component: Register },
  { path: '/reset', name: 'reset', component: Reset },
  { path: '/about', name: 'about', component: About },
  { path: '/author', name: 'author', component: Author },
  { path: '/feedback', name: 'feedback', component: Feedback },
  { path: '/task/:key', name: 'task', component: Task },
  {
    path: '/dashboard',
    name: 'dashboard',
    component: Dashboard,
    redirect: {
      name: 'tasks',
    },
    children: [
      {
        name: 'files',
        path: 'files',
        component: Files,
        meta: requireLogin,
      },
      {
        name: 'tasks',
        path: 'tasks',
        component: Tasks,
        meta: requireLogin,
      },
      // {
      //   name: 'manage',
      //   path: 'manage',
      //   component: Manage,
      //   meta: requireLogin,
      //   redirect: {
      //     name: 'overview',
      //   },
      //   children: [
      //     {
      //       name: 'overview',
      //       path: 'overview',
      //       component: Overview,
      //       meta: requireLogin,
      //     },
      //     {
      //       name: 'user',
      //       path: 'user',
      //       component: User,
      //       meta: requireLogin,
      //     },
      //   ],
      // },
    ],
  },
]
export default routes
<file_sep>/src/apis/modules/people.ts
import ajax from '../ajax'

function importPeople(key: string, filename:string, type:string) {
  return ajax.post<any, BaseResponse>(
    `/people/${key}`, {
      filename,
      type,
    },
  )
}

function getPeople(key:string) {
  return ajax.get<any, BaseResponse>(`/people/${key}`)
}

function deletePeople(key:string, id:number) {
  return ajax.delete<any, BaseResponse>(`/people/${key}`, {
    params: {
      id,
    },
  })
}

function updatePeopleStatus(key:string, filename:string, name:string, hash:string) {
  return ajax.put<any, BaseResponse>(`/people/${key}`, {
    filename,
    name,
    hash,
  })
}

function checkPeopleIsExist(key:string, name:string) {
  return ajax.get<any, BaseResponse>(`/people/check/${key}`, {
    params: {
      name,
    },
  })
}
export default {
  importPeople,
  getPeople,
  deletePeople,
  updatePeopleStatus,
  checkPeopleIsExist,
}
<file_sep>/src/@types/model.d.ts
interface TaskInfo{
    template?:string
    rewrite?:number
    format?:number
    info?:string
    share?:string
    ddl?:string
    people?:number
}

declare namespace qiniu {
    interface Subscription {
        unsubscribe(): void
    }

    interface SubscriptionConfig {
        next(res: any): void,
        error(err:any): void,
        complete(res:any): void
    }
    interface Observable {
        subscribe(cf: SubscriptionConfig): Subscription
    }

    type upload = (file: File, key: string, token: string) => Observable
    const upload: upload
}

interface DbFile {
    taskKey?: string
    taskName?: string
    categoryKey?: string
    name?: string
    info?: string
    hash?: string
    size?: number
    people?: string
  }
<file_sep>/src/store/modules/category.ts
import { CategoryApi } from '@/apis'
import { Module } from 'vuex'

interface State {
  categoryList: any []
}

const store: Module<State, unknown> = {
  namespaced: true,
  state() {
    return {
      categoryList: [],
    }
  },
  mutations: {
    updateCategory(state, payload) {
      state.categoryList = payload
    },
  },
  actions: {
    getCategory(context) {
      CategoryApi.getList().then((res) => {
        context.commit('updateCategory', res.data.categories)
      })
    },
    createCategory(context, name) {
      return CategoryApi.createNew(name).then((res) => {
        context.dispatch('getCategory')
        return res
      })
    },
    deleteCategory(context, k) {
      return CategoryApi.deleteOne(k).then((res) => {
        const idx = context.state.categoryList.findIndex((v) => v.k === k)
        if (idx >= 0) {
          context.state.categoryList.splice(idx, 1)
        }
        return res
      })
    },
  },
}

export default store
<file_sep>/src/apis/modules/category.ts
import ajax from '../ajax'

function getList() {
  return ajax.get<any, BaseResponse>(
    'category',
  )
}

function createNew(name:string) {
  return ajax.post<any, BaseResponse>(
    'category/create', {
      name,
    },
  )
}

function deleteOne(key:string) {
  return ajax.delete<any, BaseResponse>(`category/${key}`)
}
export default {
  getList,
  createNew,
  deleteOne,
}
<file_sep>/src/utils/stringUtil.ts
import { ElMessage } from 'element-plus'
import SparkMD5 from 'spark-md5'
import { jsonp } from './networkUtil'

/**
* 将结果写入的剪贴板
* @param {String} text
*/
export function copyRes(text:string) {
  const input = document.createElement('input')
  document.body.appendChild(input)
  input.setAttribute('value', text)
  input.select()
  if (document.execCommand('copy')) {
    document.execCommand('copy')
  }
  document.body.removeChild(input)
  ElMessage.success('结果已成功复制到剪贴板')
}

/**
* 生成短地址
* @param url
*/
export function getShortUrl(text:string):Promise<string> {
  return new Promise((resolve, reject) => {
    jsonp(`https://api.suowo.cn/api.htm?format=jsonp&url=${encodeURIComponent(text)}&key=<KEY>&expireDate=2030-03-31&domain=0`, 'shortLink', (res) => {
      const { url, err } = res
      if (err) {
        reject(err)
      }
      resolve(url)
    })
  })
}

export function base64(s:string) {
  return window.btoa(unescape(encodeURIComponent(s)))
}

export function formatDate(d:Date, fmt = 'yyyy-MM-dd hh:mm:ss') {
  const o:any = {
    'M+': d.getMonth() + 1, // 月份
    'd+': d.getDate(), // 日
    'h+': d.getHours(), // 小时
    'm+': d.getMinutes(), // 分
    's+': d.getSeconds(), // 秒
    'q+': Math.floor((d.getMonth() + 3) / 3), // 季度
    S: d.getMilliseconds(), // 毫秒
  }
  if (/(y+)/.test(fmt)) { fmt = fmt.replace(RegExp.$1, (`${d.getFullYear()}`).substr(4 - RegExp.$1.length)) }
  // eslint-disable-next-line no-restricted-syntax
  for (const k in o) { if (new RegExp(`(${k})`).test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : ((`00${o[k]}`).substr((`${o[k]}`).length))) }
  return fmt
}

export function getFileSuffix(str:string) {
  return str.slice(str.lastIndexOf('.'))
}

export function getFileMd5Hash(file:File) {
  return new Promise((resolve, reject) => {
    const blobSlice = File.prototype.slice
    const chunkSize = 2097152 // Read in chunks of 2MB
    const chunks = Math.ceil(file.size / chunkSize)
    let currentChunk = 0
    const spark = new SparkMD5.ArrayBuffer()
    const fileReader = new FileReader()

    function loadNext() {
      const start = currentChunk * chunkSize
      const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize

      fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
    }
    fileReader.onload = function (e) {
      // console.log('read chunk nr', currentChunk + 1, 'of', chunks)
      spark.append(e?.target?.result) // Append array buffer
      currentChunk += 1

      if (currentChunk < chunks) {
        loadNext()
      } else {
        // console.log('finished loading')
        const hashResult = spark.end()
        // console.info('computed hash', hashResult) // Compute hash
        resolve(hashResult)
      }
    }

    fileReader.onerror = function () {
      reject(new Error('oops, something went wrong.'))
    }

    loadNext()
  })
}

export function formatSize(size:number, pointLength?:number, units?:string[]) {
  let unit
  units = units || ['B', 'K', 'M', 'G', 'TB']
  // eslint-disable-next-line no-cond-assign
  while ((unit = units.shift()) && size > 1024) {
    size /= 1024
  }
  return (unit === 'B' ? size : size.toFixed(pointLength === undefined ? 2 : pointLength)) + unit
}
<file_sep>/src/apis/ajax.ts
import router from '@/router'
import axios from 'axios'
import { ElMessage } from 'element-plus'

const instance = axios.create({
  baseURL: '/api/',
})

/**
 * 请求拦截
 */
instance.interceptors.request.use((config) => {
  const { method, params } = config
  // 附带鉴权的token
  const headers: any = {
    token: localStorage.getItem('token'),
  }
  // 不缓存get请求
  if (method === 'get') {
    headers['Cache-Control'] = 'no-cache'
  }
  // delete请求参数放入body中
  if (method === 'delete') {
    headers['Content-type'] = 'application/json;'
    Object.assign(config, {
      data: params,
      params: {},
    })
  }

  return ({
    ...config,
    headers,
  })
})

// 跳转首页防抖
let redirectHome = true
/**
 * 响应拦截
 */
instance.interceptors.response.use((v) => {
  if (v.status === 200) {
    if (v.data.code === 0) {
      return v.data
    }
    if (v.data?.code === 3004) {
      localStorage.removeItem('token')
      if (redirectHome) {
        redirectHome = false
        ElMessage.error('登录过期,跳转首页')
        router.replace({
          name: 'home',
        })
        setTimeout(() => {
          redirectHome = true
        }, 1000)
      }
    }
    if (v?.data?.code === 500) {
      ElMessage.error(v?.data?.msg)
    }
    return Promise.reject(v.data)
  }
  ElMessage.error(v.statusText)
  return Promise.reject(v)
})
export default instance
<file_sep>/src/store/modules/task.ts
import { TaskApi } from '@/apis'
import { Module } from 'vuex'

interface State {
  taskList: any []
}

const store: Module<State, unknown> = {
  namespaced: true,
  state() {
    return {
      taskList: [],
    }
  },
  mutations: {
    updateTask(state, payload) {
      state.taskList = payload
    },
  },
  actions: {
    getTask(context) {
      TaskApi.getList().then((res) => {
        context.commit('updateTask', res.data.tasks)
      })
    },
    createTask(context, payload) {
      const { name, category } = payload
      return TaskApi.create(name, category).then((res) => {
        context.dispatch('getTask')
        return res
      })
    },
    deleteTask(context, k) {
      return TaskApi.deleteOne(k).then((res) => {
        const idx = context.state.taskList.findIndex((v) => v.key === k)
        if (idx >= 0) {
          context.state.taskList.splice(idx, 1)
        }
        return res
      })
    },
    updateTask(context, payload) {
      const { key, name, category } = payload
      return TaskApi.updateBaseInfo(key, name, category).then((res) => {
        context.dispatch('getTask')
        return res
      })
    },
  },
}

export default store
<file_sep>/src/store/modules/user.ts
import { Module } from 'vuex'

interface State {
  token: string,
  isSuperAdmin: boolean
}

const store: Module<State, unknown> = {
  namespaced: true,
  state() {
    return {
      token: localStorage.getItem('token') as string,
      isSuperAdmin: false,
    }
  },
  // 只能同步
  mutations: {
    setToken(state, payload) {
      state.token = payload
      if (payload) {
        localStorage.setItem('token', payload)
      } else {
        localStorage.removeItem('token')
      }
    },
    setSuperAdmin(state, payload) {
      state.isSuperAdmin = payload
    },
  },
}

export default store
<file_sep>/README.md
# <h1 align="center">EasyPicker2.0:轻取</h1>

<p align="center">为提高在线文件收取效率而生</p>
<p align="center">
 	<a href="https://ep.sugarat.top">
		<img src="https://img.shields.io/badge/status-updating-success.svg"
			 alt="Status">
	</a>
</p>


## 项目简介

在原有[EasyPicker1.0](https://ep.sugarat.top)的功能基础上,使用新技术进行功能的拓展

## 项目背景
校园学习或者工作场景中会出现以下几个场景:
* 每次碰到上机课的时候,都会遇到收取实验报告。
* 需要收取每个人填写的各种电子表格。
* 需要通过QQ/微信等等收集各种截图
* 类似场景还有不少就不列举了。。。

通常的方式是,通过QQ/微信/邮箱等收取,弊端显而易见,太过于麻烦且不方便整理统计。还占用电脑/手机内存。为了解决这个问题,此项目应运而生。

## [开发计划-进度](plan.md)

## 本地运行
<details>

1. clone仓库到本地
```sh
git clone https://github.com/ATQQ/easypicker2-client.git
```
2. 安装依赖
```sh
yarn
```
3. 本地启动

```sh
yarn dev
```

4. 其它指令

|    名称    | 作用             |
| :--------: | :--------------- |
|    dev     | 本地启动服务     |
|   build    | 生产环境打包     |
| build:test | 测试环境打包     |
|   serve    | 预览本地构建产物 |
|    lint    | 代码格式化       |

</details>

## 线上地址
1. 正式环境：https://ep2.sugarat.top
2. 测试环境：https://ep2.test.sugarat.top

注：两环境数据不互通,新功能会先在测试环境进行实验

## 技术栈
1. 前端:Vue3,Typescript,Vite - [模板仓库](https://github.com/ATQQ/vite-vue3-template)
2. 服务端:Typescript,Node.js - [模板仓库](https://github.com/ATQQ/node-server)

## 相关仓库
### [EasyPicker1.0](https://ep.sugarat.top/)
1. ~~服务端(Java-已弃用)：https://github.com/ATQQ/EasyPicker~~
2. 客户端(web) ：https://github.com/ATQQ/EasyPicker-webpack
3. 服务端（Node.js）：https://github.com/ATQQ/easypicker-server

### [EasyPicker2.0](https://ep2.sugarat.top)
1. 客户端(Web):https://github.com/ATQQ/easypicker2-client
2. 服务端(Node.js):https://github.com/ATQQ/easypicker2-server

## 相关文档
* 使用手册
* 更新日志
* 数据库相关
* 接口文档
## 其他链接
问卷反馈:https://www.wenjuan.com/s/UZBZJvA040/
<!-- ## 扫码反馈

![图片](https://img.cdn.sugarat.top/mdImg/MTU5Njg5NTE3MTk1Nw==596895171957) -->
## 欢迎体验分享,反馈Bug

![TIM图片20191015225526-20191015](https://img.cdn.sugarat.top/TIM图片20191015225526-20191015.gif)<file_sep>/plan.md
# 开发计划-进度
按页面划分功能
## 门户页 ok
* 导航
  * 登录
  * 注册
  * 反馈
  * 关于
  * 使用文档
  * Github
  * 联系QQ
  * 博客
* 产品简介

## 文件管理 ok
* 提交记录展示
  * 筛选
    * 分类
    * 分任务
    * 关键词
  * 分页
    * 自定义页面大小
  * 下载
    * 单个
    * 批量
    * 指定任务
  * 删除
    * 单个
    * 批量

## 任务管理 ok
* 分类
  * 创建
  * 删除
  * 筛选任务
* 任务
  * 创建
  * 删除
* 任务属性修改
  * 修改名称/分类
  * 设置DDL
  * 生成链接
  * 限制提交人员
    * 查看提交情况
    * 导出提交情况
  * 设置模板文件
  * 自动重命名
  * 必填信息

## 文件上传 ok
* 提交文件
  * 单个
  * 多个
* 填写必要信息
* 下载模板
* 撤回提交的文件
* 查看截止日期

## 登录 ok
* 导航
  * 忘记密码(修改/重置密码)
  * 注册
* 登录方式
  * 手机号-密码
  * 账号-密码
  * 手机号-验证码
* 登录成功
  * 数据看板页
  * 文件管理
  * 任务管理

## 注册 ok
* 方式
  * 账号+密码
  * 账号+密码+手机号

## 忘记密码 ok
* 修改方式
  * 手机号+验证码+新密码

## 数据看板
* 产品运行信息查看
* 所有操作日志,提交记录

## 用户管理
* 基本信息维护
* 状态信息修改
* 行为日志查看

## 反馈页
* 上报问题

## 反馈查看页面
* 更进问题与处理进度
  
...未完待续<file_sep>/src/apis/modules/task.ts
import ajax from '../ajax'

function getList() {
  return ajax.get<any, BaseResponse>('task')
}

function create(name:string, category:string) {
  return ajax.post<any, BaseResponse>('task/create', {
    name,
    category,
  })
}

function deleteOne(key:string) {
  return ajax.delete<any, BaseResponse>(`task/${key}`)
}

function updateBaseInfo(key:string, name:string, category:string) {
  return ajax.put<any, BaseResponse>(`task/${key}`, {
    name,
    category,
  })
}

function getTaskInfo(key:string) {
  return ajax.get<any, BaseResponse>(`task/${key}`)
}

function getTaskMoreInfo(key:string) {
  return ajax.get<any, BaseResponse>(`task_info/${key}`)
}

function updateTaskMoreInfo(key:string, options:any) {
  return ajax.put<any, BaseResponse>(`task_info/${key}`, options)
}
export default {
  getList,
  create,
  deleteOne,
  updateBaseInfo,
  getTaskInfo,
  getTaskMoreInfo,
  updateTaskMoreInfo,
}
<file_sep>/src/apis/modules/user.ts
import ajax from '../ajax'

function register(account:string, pwd:string, bindPhone = false, options?:any) {
  return ajax.post<any, BaseResponse>('user/register', {
    account,
    pwd,
    bindPhone,
    ...options,
  })
}

function login(account:string, pwd:string) {
  return ajax.post<any, BaseResponse>('user/login', {
    account, pwd,
  })
}

function codeLogin(phone:string, code:string) {
  return ajax.post<any, BaseResponse>('user/login/code', {
    phone, code,
  })
}

function resetPwd(phone:string, code:string, pwd:string) {
  return ajax.put<any, BaseResponse>('user/password', {
    phone, code, pwd,
  })
}

function checkPower() {
  return ajax.get<any, BaseResponse>('user/power/super')
}
export default {
  register,
  login,
  codeLogin,
  resetPwd,
  checkPower,
}
<file_sep>/src/apis/modules/super/overview.ts
import ajax from '../../ajax'

const baseUrl = '/super/overview'
function getCount() {
  return ajax.get<any, BaseResponse>(`${baseUrl}/count`)
}

function getAllLogMsg() {
  return ajax.get<any, BaseResponse>(`${baseUrl}/log`)
}
export default {
  getCount,
  getAllLogMsg,
}
<file_sep>/src/utils/elementUI.ts
import { App } from '@vue/runtime-core'

// import {
//   ElButton, ElInput, ElPopover, ElTag,
//   ElSelect, ElOption, ElCard, ElEmpty,
//   ElForm, ElFormItem, ElDialog, ElImage,
//   ElTabs, ElTabPane, ElDatePicker, ElTimePicker, ElTimeSelect,
// } from 'element-plus'
// import 'element-plus/packages/theme-chalk/src/icon.scss'

// import lang from 'element-plus/lib/locale/lang/zh-cn'
// import 'dayjs/locale/zh-cn'
// import locale from 'element-plus/lib/locale'

// // 设置语言
// locale.use(lang)

// const components = [
//   ElSelect,
//   ElOption,
//   ElCard,
//   ElEmpty, ElImage,
//   ElTabs, ElTabPane,
//   ElDatePicker,
//   ElTimePicker,
//   ElTimeSelect,
//   ElDatePicker,
//   ElForm, ElFormItem, ElDialog,
//   ElButton, ElInput, ElPopover, ElTag]
import ElementPlus from 'element-plus'
import 'element-plus/lib/theme-chalk/index.css'
import 'dayjs/locale/zh-cn'
import locale from 'element-plus/lib/locale/lang/zh-cn'

export default function mountElementUI(app: App<Element>) {
  app.use(ElementPlus, { locale })
  // components.forEach((c) => {
  //   app.component(c.name, c)
  // })
}
<file_sep>/src/apis/index.ts
import p from './modules/public'
import user from './modules/user'
import category from './modules/category'
import task from './modules/task'
import people from './modules/people'
import file from './modules/file'
import superOverview from './modules/super/overview'

export const PublicApi = p
export const UserApi = user
export const CategoryApi = category
export const TaskApi = task
export const PeopleApi = people
export const FileApi = file
export const SuperOverviewApi = superOverview
<file_sep>/build/deploy.sh
# /bin/bash
compressFile="ep2.tar.gz"        # 压缩后的文件名
user="root"                         # 远程登录用户
origin="sugarat.top"                   # 远程登录origin
targetDir="/www/wwwroot/ep2.sugarat.top"     # 目标目录
echo "开始-----部署"
ssh -p22 ${user}@${origin} "tar -zvxf ${compressFile} -C ${targetDir}"
echo "清理-----临时的文件"
rm -rf $compressFile<file_sep>/src/store/index.ts
import { createStore } from 'vuex'
import user from './modules/user'
import category from './modules/category'
import task from './modules/task'

// Create a new store instance.
const store = createStore({
  modules: {
    user,
    category,
    task,
  },
})

export default store
<file_sep>/src/main.ts
import { createApp } from 'vue'
import mountElementUI from './utils/elementUI'

import router from './router'
import store from './store'

import App from './App.vue'
import Axios from './apis/ajax'

document.title = import.meta.env.VITE_APP_TITLE as string

const app = createApp(App)

app.provide('$http', Axios)

app.use(router)
app.use(store)
// 引入需要的ElementUI Component
mountElementUI(app)

app.mount('#app')
