├── .env.development ├── .env.production ├── .github └── workflows │ └── docker-ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc.js ├── Dockerfile ├── LICENSE ├── README.md ├── auto-imports.d.ts ├── babel.config.js ├── commitlint.config.js ├── components.d.ts ├── config ├── plugin │ ├── arcoResolver.ts │ ├── arcoStyleImport.ts │ ├── compress.ts │ ├── imagemin.ts │ └── visualizer.ts ├── utils │ └── index.ts ├── vite.config.base.ts ├── vite.config.dev.ts └── vite.config.prod.ts ├── docker-compose.yml ├── forge.config.js ├── icon.ico ├── index.html ├── main.js ├── package-lock.json ├── package.json ├── public ├── 1714006925783.jpg ├── 1722745910257.jpg ├── CNAME ├── K5Channel.xlsx ├── K5Channel_EN.xlsx ├── LOSEHU117P6.bin ├── LOSEHU117P6K.bin ├── LOSEHU126.bin ├── LOSEHU126H.bin ├── LOSEHU126K.bin ├── L_BL001.bin ├── O1CN019Pat6v1ZuNgxL8CRt_!!6000000003254-0-tps-800-450.jpg ├── adimg1c.png ├── adimg2c.png ├── gy.png ├── img1.png ├── img2.png ├── img3.png ├── img4.png ├── img5.png ├── img6.png ├── jjgg.jpg ├── k5web.png ├── mm_facetoface_collect_qrcode_1714392837792.png ├── new_font_h.bin ├── new_font_k.bin ├── new_font_k_f.bin ├── old_font.bin ├── pinyin.bin ├── pinyin_plus.bin ├── qrcode_1714310463601.jpg ├── rhino-design-800x450.png ├── serial.js ├── sms_test.bin └── ssb.bin ├── src ├── App.vue ├── api │ ├── interceptor.ts │ └── user.ts ├── assets │ ├── images │ │ └── login-banner.png │ ├── logo.svg │ ├── style │ │ ├── breakpoint.less │ │ └── global.less │ └── world.json ├── components │ ├── breadcrumb │ │ └── index.vue │ ├── chart │ │ └── index.vue │ ├── footer │ │ └── index.vue │ ├── global-setting │ │ ├── block.vue │ │ ├── form-wrapper.vue │ │ └── index.vue │ ├── index.ts │ ├── menu │ │ ├── index.vue │ │ └── use-menu-tree.ts │ ├── navbar │ │ └── index.vue │ └── tab-bar │ │ ├── index.vue │ │ ├── readme.md │ │ └── tab-item.vue ├── config │ └── settings.json ├── directive │ ├── index.ts │ └── permission │ │ └── index.ts ├── drivers │ ├── losehu117.json │ ├── losehu117k.json │ ├── losehu118.json │ ├── losehu118h.json │ ├── losehu118k.json │ ├── losehu120k.json │ ├── losehu124h.json │ ├── losehubl.json │ ├── losehud.json │ ├── lts.json │ ├── ltsk.json │ └── todo.json ├── env.d.ts ├── hooks │ ├── chart-option.ts │ ├── loading.ts │ ├── locale.ts │ ├── permission.ts │ ├── request.ts │ ├── responsive.ts │ ├── themes.ts │ ├── user.ts │ └── visible.ts ├── layout │ ├── default-layout.vue │ └── page-layout.vue ├── locale │ ├── en-US.ts │ ├── en-US │ │ └── settings.ts │ ├── index.ts │ ├── zh-CN.ts │ └── zh-CN │ │ └── settings.ts ├── main.ts ├── router │ ├── app-menus │ │ └── index.ts │ ├── constants.ts │ ├── guard │ │ ├── index.ts │ │ ├── permission.ts │ │ └── userLoginInfo.ts │ ├── index.ts │ ├── routes │ │ ├── base.ts │ │ ├── externalModules │ │ │ └── faq.ts │ │ ├── index.ts │ │ ├── modules │ │ │ ├── dashboard.ts │ │ │ ├── idea.ts │ │ │ └── list.ts │ │ └── types.ts │ └── typings.d.ts ├── store │ ├── index.ts │ └── modules │ │ ├── app │ │ ├── index.ts │ │ └── types.ts │ │ ├── tab-bar │ │ ├── index.ts │ │ └── types.ts │ │ └── user │ │ ├── index.ts │ │ └── types.ts ├── types │ ├── echarts.ts │ ├── global.ts │ └── mock.ts ├── utils │ ├── AutoUpdate.js │ ├── auth.ts │ ├── env.ts │ ├── event.ts │ ├── index.ts │ ├── is.ts │ ├── monitor.ts │ ├── route-listener.ts │ ├── serial.js │ └── setup-mock.ts └── views │ ├── dashboard │ └── workplace │ │ ├── components │ │ └── banner.vue │ │ ├── index.vue │ │ └── locale │ │ ├── en-US.ts │ │ └── zh-CN.ts │ ├── guide │ └── f117 │ │ ├── assets │ │ ├── cj1.png │ │ ├── cj2.png │ │ └── cj3.png │ │ ├── index.vue │ │ └── locale │ │ ├── en-US.ts │ │ └── zh-CN.ts │ ├── idea │ ├── channel │ │ └── index.vue │ ├── firmware │ │ └── index.vue │ ├── image │ │ └── index.vue │ └── losehu │ │ └── index.vue │ ├── list │ ├── bl │ │ └── index.vue │ ├── card │ │ ├── index.vue │ │ └── locale │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ ├── chat │ │ └── index.vue │ ├── chi │ │ ├── index.vue │ │ └── locale │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ ├── dtmf │ │ └── index.vue │ ├── flash │ │ ├── index.vue │ │ └── locale │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ ├── image │ │ ├── index.vue │ │ └── locale │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ ├── mdc │ │ └── index.vue │ ├── radio │ │ └── index.vue │ ├── sat │ │ ├── index.vue │ │ └── locale │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ ├── sat2 │ │ └── index.vue │ ├── satloc │ │ └── index.vue │ ├── search-table │ │ ├── index.vue │ │ └── locale │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ └── settings │ │ ├── index.vue │ │ └── locale │ │ ├── en-US.ts │ │ └── zh-CN.ts │ ├── not-found │ └── index.vue │ ├── redirect │ └── index.vue │ └── thanks │ └── index.vue ├── tsconfig.json └── yarn.lock /.env.development: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL= 'http://localhost:8080' 2 | VITE_METER_SITE = '' 3 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_METER_SITE = 'k5.vicicode.com,k5.vicicode.cn,k6.vicicode.cn,k5.lhw711.cn,mm.md,k5.mm.md,k5.losehu.com,k6.losehu.com' 2 | -------------------------------------------------------------------------------- /.github/workflows/docker-ci.yml: -------------------------------------------------------------------------------- 1 | name: k5web-docker-ci 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | PLATFORMS: linux/amd64 7 | TAG: latest 8 | 9 | permissions: 10 | packages: write 11 | 12 | jobs: 13 | main: 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | configuration: [Release] 18 | file: [Dockerfile] 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v4.1.7 25 | with: 26 | show-progress: false 27 | submodules: recursive 28 | 29 | 30 | - name: Login to ghcr.io 31 | uses: docker/login-action@v3.2.0 32 | with: 33 | registry: ghcr.io 34 | username: ${{ github.actor }} 35 | password: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v3.3.0 39 | 40 | - name: Prepare environment outputs 41 | shell: sh 42 | run: | 43 | set -eu 44 | 45 | echo "DATE_ISO8601=$(date --iso-8601=seconds --utc)" >> "$GITHUB_ENV" 46 | echo "FIXED_TAG=$(echo ${{ github.ref }} | cut -d '/' -f 3)" >> "$GITHUB_ENV" 47 | echo "GHCR_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" 48 | echo "SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-8)" >> $GITHUB_ENV 49 | 50 | - name: Build ${{ matrix.configuration }} Docker image from ${{ matrix.file }} 51 | uses: docker/build-push-action@v6.0.0 52 | with: 53 | build-args: CONFIGURATION=${{ matrix.configuration }} 54 | context: . 55 | file: ${{ matrix.file }} 56 | platforms: ${{ env.PLATFORMS }} 57 | labels: | 58 | org.opencontainers.image.created=${{ env.DATE_ISO8601 }} 59 | org.opencontainers.image.version=${{ env.FIXED_TAG }} 60 | org.opencontainers.image.revision=${{ github.sha }} 61 | tags: | 62 | ghcr.io/${{ env.GHCR_REPOSITORY }}:${{ env.SHORT_SHA }} 63 | ghcr.io/${{ env.GHCR_REPOSITORY }}:${{ env.TAG }} 64 | provenance: true 65 | sbom: true 66 | push: true 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | node_modules 7 | .DS_Store 8 | dist 9 | dist-ssr 10 | *.local 11 | out 12 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | .local 3 | .output.js 4 | /node_modules/** 5 | 6 | **/*.svg 7 | **/*.sh -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | semi: true, 4 | printWidth: 80, 5 | singleQuote: true, 6 | quoteProps: 'consistent', 7 | htmlWhitespaceSensitivity: 'strict', 8 | vueIndentScriptAndStyle: true, 9 | }; 10 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'stylelint-config-standard', 4 | 'stylelint-config-rational-order', 5 | 'stylelint-config-prettier', 6 | 'stylelint-config-recommended-vue', 7 | ], 8 | defaultSeverity: 'warning', 9 | plugins: ['stylelint-order'], 10 | rules: { 11 | 'at-rule-no-unknown': [ 12 | true, 13 | { 14 | ignoreAtRules: ['plugin'], 15 | }, 16 | ], 17 | 'rule-empty-line-before': [ 18 | 'always', 19 | { 20 | except: ['after-single-line-comment', 'first-nested'], 21 | }, 22 | ], 23 | 'selector-pseudo-class-no-unknown': [ 24 | true, 25 | { 26 | ignorePseudoClasses: ['deep'], 27 | }, 28 | ], 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 AS build-yarn 2 | 3 | WORKDIR /app 4 | 5 | COPY yarn.lock package.json ./ 6 | 7 | RUN yarn install 8 | 9 | COPY . . 10 | 11 | RUN yarn build 12 | 13 | FROM nginx:latest AS runtime 14 | 15 | COPY --from=build-yarn /app/dist/ /usr/share/nginx/html/ 16 | 17 | EXPOSE 80 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024-2025 Silent YANG 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # K5Web 2 | 3 | - 世界业余无线电日 K5Web 正式开源,添加开源许可协议。 4 | - 感谢所有 HAM 。 5 | 6 | ## 简介 7 | 8 | K5Web 用于对兼容业余无线电台 UV-K5 写频、更新固件、写入星历等。 9 | 10 | ## 讨论 11 | - QQ 群:957225277 (K5Web相关) 12 | - QQ 群:201308015 (固件相关) 13 | - Telegram Group: https://t.me/losehu 14 | - Matrix Group: https://matrix.to/#/#losehu:mozilla.org 15 | 16 | ## 功能列表 17 | 18 | - 固件版本检测 19 | - EEPROM 大小检测 20 | - 信道管理 21 | - 启动画面文字管理 22 | - MDC 本地侧音控制(仅支持我的 LTS 固件) 23 | - 备份/还原 EEPROM 24 | - 固件升级 25 | - 开机图片(LOSEHU 固件) 26 | - 字库写入(LOSEHU 固件) 27 | - 星历写入(LOSEHU 固件) 28 | - DTMF ID 设置 29 | - 收音机频道管理 30 | - MDC 联系人管理(LOSEHU 固件) 31 | 32 | ## 开发 33 | ### 安装依赖 34 | ``` 35 | yarn 36 | ``` 37 | ### 开发 38 | ``` 39 | yarn dev 40 | ``` 41 | ### 编译 42 | ``` 43 | yarn build 44 | ``` 45 | 46 | ## 关联项目 47 | ### 星历计算接口: 48 | https://github.com/silenty4ng/k5sat 49 | 50 | ### 我的固件: 51 | https://github.com/silenty4ng/uv-k5-firmware-chinese-lts 52 | 53 | ## 感谢项目 54 | - https://github.com/whosmatt/uvmod 55 | - https://github.com/egzumer/uvtools 56 | - https://github.com/losehu/uv-k5-firmware-custom 57 | - https://github.com/selevo/WebUsbSerialTerminal 58 | - https://github.com/fagci/uvk5-manager 59 | - https://github.com/hank9999/K5_Tools 60 | - https://github.com/kk7ds/chirp 61 | 62 | ## 开源协议 63 | 64 | ``` 65 | Copyright (c) 2024 Silent YANG 66 | 67 | Permission is hereby granted, free of charge, to any person obtaining a copy 68 | of this software and associated documentation files (the "Software"), to deal 69 | in the Software without restriction, including without limitation the rights 70 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 71 | copies of the Software, and to permit persons to whom the Software is 72 | furnished to do so, subject to the following conditions: 73 | 74 | The above copyright notice and this permission notice shall be included in all 75 | copies or substantial portions of the Software. 76 | 77 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 78 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 79 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 80 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 81 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 82 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 83 | SOFTWARE. 84 | ``` 85 | 86 | ## Star History 87 | 88 | [![Star History Chart](https://api.star-history.com/svg?repos=silenty4ng/k5web&type=Date)](https://star-history.com/#silenty4ng/k5web&Date) 89 | 90 | ## 饿饿饭饭 91 | 92 | 93 | TRON / TRX:TPaSnHJ2cRCQjjv7TyAFJDamb3mZSSz1At 94 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['@vue/babel-plugin-jsx'], 3 | }; 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | Block: typeof import('./src/components/global-setting/block.vue')['default'] 11 | Breadcrumb: typeof import('./src/components/breadcrumb/index.vue')['default'] 12 | Chart: typeof import('./src/components/chart/index.vue')['default'] 13 | Footer: typeof import('./src/components/footer/index.vue')['default'] 14 | FormWrapper: typeof import('./src/components/global-setting/form-wrapper.vue')['default'] 15 | GlobalSetting: typeof import('./src/components/global-setting/index.vue')['default'] 16 | List: typeof import('./src/components/message-box/list.vue')['default'] 17 | Menu: typeof import('./src/components/menu/index.vue')['default'] 18 | MessageBox: typeof import('./src/components/message-box/index.vue')['default'] 19 | Navbar: typeof import('./src/components/navbar/index.vue')['default'] 20 | RouterLink: typeof import('vue-router')['RouterLink'] 21 | RouterView: typeof import('vue-router')['RouterView'] 22 | TabBar: typeof import('./src/components/tab-bar/index.vue')['default'] 23 | TabItem: typeof import('./src/components/tab-bar/tab-item.vue')['default'] 24 | TButton: typeof import('tdesign-vue-next')['Button'] 25 | TCard: typeof import('tdesign-vue-next')['Card'] 26 | TCheckbox: typeof import('tdesign-vue-next')['Checkbox'] 27 | TCheckboxGroup: typeof import('tdesign-vue-next')['CheckboxGroup'] 28 | TCol: typeof import('tdesign-vue-next')['Col'] 29 | TConfigProvider: typeof import('tdesign-vue-next')['ConfigProvider'] 30 | TDialog: typeof import('tdesign-vue-next')['Dialog'] 31 | TDrawer: typeof import('tdesign-vue-next')['Drawer'] 32 | TForm: typeof import('tdesign-vue-next')['Form'] 33 | TFormItem: typeof import('tdesign-vue-next')['FormItem'] 34 | TInput: typeof import('tdesign-vue-next')['Input'] 35 | TLink: typeof import('tdesign-vue-next')['Link'] 36 | TList: typeof import('tdesign-vue-next')['List'] 37 | TListItem: typeof import('tdesign-vue-next')['ListItem'] 38 | TPagination: typeof import('tdesign-vue-next')['Pagination'] 39 | TRow: typeof import('tdesign-vue-next')['Row'] 40 | TSpace: typeof import('tdesign-vue-next')['Space'] 41 | TSwitch: typeof import('tdesign-vue-next')['Switch'] 42 | TTable: typeof import('tdesign-vue-next')['Table'] 43 | TTag: typeof import('tdesign-vue-next')['Tag'] 44 | TTextarea: typeof import('tdesign-vue-next')['Textarea'] 45 | TUpload: typeof import('tdesign-vue-next')['Upload'] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/plugin/arcoResolver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * If you use the template method for development, you can use the unplugin-vue-components plugin to enable on-demand loading support. 3 | * 按需引入 4 | * https://github.com/antfu/unplugin-vue-components 5 | * https://arco.design/vue/docs/start 6 | * Although the Pro project is full of imported components, this plugin will be used by default. 7 | * 虽然Pro项目中是全量引入组件,但此插件会默认使用。 8 | */ 9 | import Components from 'unplugin-vue-components/vite'; 10 | import { ArcoResolver } from 'unplugin-vue-components/resolvers'; 11 | 12 | export default function configArcoResolverPlugin() { 13 | const arcoResolverPlugin = Components({ 14 | dirs: [], // Avoid parsing src/components. 避免解析到src/components 15 | deep: false, 16 | resolvers: [ArcoResolver()], 17 | }); 18 | return arcoResolverPlugin; 19 | } 20 | -------------------------------------------------------------------------------- /config/plugin/arcoStyleImport.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Theme import 3 | * 样式按需引入 4 | * https://github.com/arco-design/arco-plugins/blob/main/packages/plugin-vite-vue/README.md 5 | * https://arco.design/vue/docs/start 6 | */ 7 | import { vitePluginForArco } from '@arco-plugins/vite-vue'; 8 | 9 | export default function configArcoStyleImportPlugin() { 10 | const arcoResolverPlugin = vitePluginForArco({}); 11 | return arcoResolverPlugin; 12 | } 13 | -------------------------------------------------------------------------------- /config/plugin/compress.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated 3 | * gzip压缩 4 | * https://github.com/anncwb/vite-plugin-compression 5 | */ 6 | import type { Plugin } from 'vite'; 7 | import compressPlugin from 'vite-plugin-compression'; 8 | 9 | export default function configCompressPlugin( 10 | compress: 'gzip' | 'brotli', 11 | deleteOriginFile = false 12 | ): Plugin | Plugin[] { 13 | const plugins: Plugin[] = []; 14 | 15 | if (compress === 'gzip') { 16 | plugins.push( 17 | compressPlugin({ 18 | ext: '.gz', 19 | deleteOriginFile, 20 | }) 21 | ); 22 | } 23 | 24 | if (compress === 'brotli') { 25 | plugins.push( 26 | compressPlugin({ 27 | ext: '.br', 28 | algorithm: 'brotliCompress', 29 | deleteOriginFile, 30 | }) 31 | ); 32 | } 33 | return plugins; 34 | } 35 | -------------------------------------------------------------------------------- /config/plugin/imagemin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Image resource files used to compress the output of the production environment 3 | * 图片压缩 4 | * https://github.com/anncwb/vite-plugin-imagemin 5 | */ 6 | import viteImagemin from 'vite-plugin-imagemin'; 7 | 8 | export default function configImageminPlugin() { 9 | const imageminPlugin = viteImagemin({ 10 | gifsicle: { 11 | optimizationLevel: 7, 12 | interlaced: false, 13 | }, 14 | optipng: { 15 | optimizationLevel: 7, 16 | }, 17 | mozjpeg: { 18 | quality: 20, 19 | }, 20 | pngquant: { 21 | quality: [0.8, 0.9], 22 | speed: 4, 23 | }, 24 | svgo: { 25 | plugins: [ 26 | { 27 | name: 'removeViewBox', 28 | }, 29 | { 30 | name: 'removeEmptyAttrs', 31 | active: false, 32 | }, 33 | ], 34 | }, 35 | }); 36 | return imageminPlugin; 37 | } 38 | -------------------------------------------------------------------------------- /config/plugin/visualizer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generation packaging analysis 3 | * 生成打包分析 4 | */ 5 | import visualizer from 'rollup-plugin-visualizer'; 6 | import { isReportMode } from '../utils'; 7 | 8 | export default function configVisualizerPlugin() { 9 | if (isReportMode()) { 10 | return visualizer({ 11 | filename: './node_modules/.cache/visualizer/stats.html', 12 | open: true, 13 | gzipSize: true, 14 | brotliSize: true, 15 | }); 16 | } 17 | return []; 18 | } 19 | -------------------------------------------------------------------------------- /config/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Whether to generate package preview 3 | * 是否生成打包报告 4 | */ 5 | export default {}; 6 | 7 | export function isReportMode(): boolean { 8 | return process.env.REPORT === 'true'; 9 | } 10 | -------------------------------------------------------------------------------- /config/vite.config.base.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig } from 'vite'; 3 | import vue from '@vitejs/plugin-vue'; 4 | import vueJsx from '@vitejs/plugin-vue-jsx'; 5 | import svgLoader from 'vite-svg-loader'; 6 | import configArcoStyleImportPlugin from './plugin/arcoStyleImport'; 7 | 8 | import AutoImport from 'unplugin-auto-import/vite'; 9 | import Components from 'unplugin-vue-components/vite'; 10 | import { TDesignResolver } from 'unplugin-vue-components/resolvers'; 11 | import { ArcoResolver } from 'unplugin-vue-components/resolvers'; 12 | 13 | import htmlPlugin from "vite-plugin-html-config"; 14 | 15 | export default defineConfig({ 16 | base: './', 17 | plugins: [ 18 | vue(), 19 | vueJsx(), 20 | svgLoader({ svgoConfig: {} }), 21 | configArcoStyleImportPlugin(), 22 | AutoImport({ 23 | resolvers: [TDesignResolver({ 24 | library: 'vue-next' 25 | })], 26 | }), 27 | Components({ 28 | resolvers: [TDesignResolver({ 29 | library: 'vue-next' 30 | }), ArcoResolver()], 31 | }), 32 | htmlPlugin({ 33 | metas: [ 34 | { 35 | name: "builtTime", 36 | content: Math.ceil(parseInt(new Date().toISOString().replace(/[.:TZ-]/g, '')) / 1000).toString() 37 | }, 38 | ] 39 | }) 40 | ], 41 | resolve: { 42 | alias: [ 43 | { 44 | find: '@', 45 | replacement: resolve(__dirname, '../src'), 46 | }, 47 | { 48 | find: 'assets', 49 | replacement: resolve(__dirname, '../src/assets'), 50 | }, 51 | { 52 | find: 'vue-i18n', 53 | replacement: 'vue-i18n/dist/vue-i18n.cjs.js', // Resolve the i18n warning issue 54 | }, 55 | { 56 | find: 'vue', 57 | replacement: 'vue/dist/vue.esm-bundler.js', // compile template 58 | }, 59 | ], 60 | extensions: ['.ts', '.js', '.css'], 61 | }, 62 | define: { 63 | 'process.env': {}, 64 | }, 65 | css: { 66 | preprocessorOptions: { 67 | less: { 68 | modifyVars: { 69 | hack: `true; @import (reference) "${resolve( 70 | 'src/assets/style/breakpoint.less' 71 | )}";`, 72 | }, 73 | javascriptEnabled: true, 74 | }, 75 | }, 76 | postcss: { 77 | plugins: [ 78 | require('postcss-px-to-viewport')({ 79 | viewportWidth: 2560, // 视口宽度,对应设计稿宽度 80 | viewporHeight: 1440, // 视口高度,对应设计稿高度 81 | unitPrecision: 3, // 指定px转换之后的小数位数 82 | viewportUnit: 'vw', // 转换的单位 83 | fontViewportUnit: 'vw', // 字体使用的单位 84 | replace: false, // 是否直接更换属性值,而不添加备用属性 85 | selectorBlackList: ['.ignore', '.hairlines', '.arco', '.layout', '.nav-btn'], // 指定不转换的类 86 | exclude: /(\/|\\)(node_modules)(\/|\\)/, //禁止更改第三方UI框架样式 87 | minPixelValue: 15, // 小于或等于1px不转换 88 | mediaQuery: true, // 允许在媒体查询中转换 89 | }) 90 | ] 91 | } 92 | }, 93 | }); 94 | -------------------------------------------------------------------------------- /config/vite.config.dev.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig } from 'vite'; 2 | import baseConfig from './vite.config.base'; 3 | 4 | export default mergeConfig( 5 | { 6 | mode: 'development', 7 | server: { 8 | host: "0.0.0.0", 9 | open: false, 10 | fs: { 11 | strict: true, 12 | }, 13 | }, 14 | plugins: [], 15 | }, 16 | baseConfig 17 | ); 18 | -------------------------------------------------------------------------------- /config/vite.config.prod.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig } from 'vite'; 2 | import baseConfig from './vite.config.base'; 3 | import configCompressPlugin from './plugin/compress'; 4 | import configVisualizerPlugin from './plugin/visualizer'; 5 | import configImageminPlugin from './plugin/imagemin'; 6 | 7 | export default mergeConfig( 8 | { 9 | mode: 'production', 10 | plugins: [ 11 | configCompressPlugin('gzip'), 12 | configVisualizerPlugin(), 13 | configImageminPlugin(), 14 | ], 15 | build: { 16 | rollupOptions: { 17 | output: { 18 | manualChunks(id) { 19 | if (id.includes("node_modules")) { 20 | // 让每个插件都打包成独立的文件 21 | return id .toString() .split("node_modules/")[1] .split("/")[0] .toString(); 22 | } 23 | } 24 | }, 25 | }, 26 | chunkSizeWarningLimit: 2000, 27 | }, 28 | }, 29 | baseConfig 30 | ); 31 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | services: 3 | k5web: 4 | image: ghcr.io/silenty4ng/k5web:latest 5 | container_name: k5web 6 | network_mode: "bridge" 7 | ports: 8 | - "5173:80" 9 | environment: 10 | - TZ=Asia/Shanghai 11 | restart: always 12 | -------------------------------------------------------------------------------- /forge.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | packagerConfig: { 3 | asar: true, 4 | }, 5 | rebuildConfig: {}, 6 | makers: [ 7 | { 8 | name: '@electron-forge/maker-squirrel', 9 | config: {}, 10 | }, 11 | { 12 | name: '@electron-forge/maker-zip', 13 | platforms: ['darwin'], 14 | }, 15 | { 16 | name: '@electron-forge/maker-deb', 17 | config: {}, 18 | }, 19 | { 20 | name: '@electron-forge/maker-rpm', 21 | config: {}, 22 | }, 23 | { 24 | name: '@rabbitholesyndrome/electron-forge-maker-portable', 25 | config: { 26 | appId: 'com.vicicode.k5web', 27 | productName: 'K5Web', 28 | icon: 'icon.ico' 29 | }, 30 | portable: { 31 | artifactName: 'k5web.exe' 32 | } 33 | } 34 | ], 35 | plugins: [ 36 | { 37 | name: '@electron-forge/plugin-auto-unpack-natives', 38 | config: {}, 39 | }, 40 | ], 41 | }; 42 | -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/icon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | K5Web 9 | 10 | 11 | 28 | 124 | 125 | 126 | 127 | 128 |
129 |
130 |
131 | 132 |
133 |
134 |
135 | 136 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, dialog } = require('electron/main'); 2 | 3 | function createWindow() { 4 | const mainWindow = new BrowserWindow({ 5 | width: 1280, 6 | height: 760, 7 | autoHideMenuBar: true 8 | }); 9 | 10 | mainWindow.webContents.session.on('select-serial-port', async (event, portList, webContents, callback) => { 11 | // Add listeners to handle ports being added or removed before the callback for `select-serial-port` is called. 12 | mainWindow.webContents.session.on('serial-port-added', (event, port) => { 13 | console.log('serial-port-added FIRED WITH', port); 14 | // Optionally update portList to add the new port 15 | }); 16 | 17 | mainWindow.webContents.session.on('serial-port-removed', (event, port) => { 18 | console.log('serial-port-removed FIRED WITH', port); 19 | // Optionally update portList to remove the port 20 | }); 21 | 22 | event.preventDefault(); 23 | 24 | if (portList && portList.length > 0) { 25 | // Only keep the last 5 ports 26 | const lastFivePorts = portList.slice(-5); 27 | 28 | // Prepare options for dialog box 29 | const options = { 30 | type: 'question', 31 | buttons: lastFivePorts.map(port => port.portName), // Display port names as choices 32 | title: 'Select Serial Port', 33 | message: 'Please select a serial port to use:' 34 | }; 35 | 36 | const result = await dialog.showMessageBox(mainWindow, options); 37 | if (result.response >= 0) { 38 | callback(lastFivePorts[result.response].portId); // Callback with the selected port ID 39 | } else { 40 | callback(''); // No port selected 41 | } 42 | } else { 43 | callback(''); // No ports available 44 | } 45 | }); 46 | 47 | mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => { 48 | if (permission === 'serial' && details.securityOrigin === 'file:///') { 49 | return true; 50 | } 51 | 52 | return false; 53 | }); 54 | 55 | mainWindow.webContents.session.setDevicePermissionHandler((details) => { 56 | if (details.deviceType === 'serial' && details.origin === 'file://') { 57 | return true; 58 | } 59 | 60 | return false; 61 | }); 62 | 63 | mainWindow.loadFile('./dist/index.html'); 64 | 65 | // mainWindow.webContents.openDevTools(); 66 | } 67 | 68 | app.whenReady().then(() => { 69 | createWindow(); 70 | 71 | app.on('activate', function () { 72 | if (BrowserWindow.getAllWindows().length === 0) createWindow(); 73 | }); 74 | }); 75 | 76 | app.on('window-all-closed', function () { 77 | if (process.platform !== 'darwin') app.quit(); 78 | }); 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "k5web", 3 | "description": "K5Web", 4 | "version": "1.0.0", 5 | "private": true, 6 | "author": "Silent YANG", 7 | "license": "MIT", 8 | "main": "main.js", 9 | "scripts": { 10 | "dev": "vite --config ./config/vite.config.dev.ts", 11 | "build": "vite build --config ./config/vite.config.prod.ts", 12 | "report": "cross-env REPORT=true npm run build", 13 | "preview": "npm run build && vite preview --host", 14 | "start": "electron-forge start", 15 | "package": "electron-forge package", 16 | "make": "electron-forge make" 17 | }, 18 | "dependencies": { 19 | "@arco-design/web-vue": "^2.44.7", 20 | "@vueuse/core": "^9.3.0", 21 | "@zxing/text-encoding": "^0.9.0", 22 | "aegis-web-sdk": "^1.39.1", 23 | "axios": "^0.24.0", 24 | "chinese-s2t": "^1.0.0", 25 | "dayjs": "^1.11.5", 26 | "dompurify": "^3.1.7", 27 | "echarts": "^5.4.0", 28 | "electron-squirrel-startup": "^1.0.0", 29 | "lodash": "^4.17.21", 30 | "marked": "^14.1.2", 31 | "mitt": "^3.0.0", 32 | "nprogress": "^0.2.0", 33 | "pinia": "^2.0.23", 34 | "qrcode": "^1.5.3", 35 | "query-string": "^8.0.3", 36 | "sortablejs": "^1.15.0", 37 | "tdesign-vue-next": "^1.9.4", 38 | "uuid": "^9.0.1", 39 | "vue": "^3.2.40", 40 | "vue-echarts": "^6.2.3", 41 | "vue-i18n": "^9.2.2", 42 | "vue-matomo": "^4.2.0", 43 | "vue-router": "^4.0.14", 44 | "xlsx": "^0.18.5" 45 | }, 46 | "devDependencies": { 47 | "@arco-plugins/vite-vue": "^1.4.5", 48 | "@commitlint/cli": "^17.1.2", 49 | "@commitlint/config-conventional": "^17.1.0", 50 | "@electron-forge/cli": "^7.2.0", 51 | "@electron-forge/maker-deb": "^7.2.0", 52 | "@electron-forge/maker-rpm": "^7.2.0", 53 | "@electron-forge/maker-squirrel": "^7.2.0", 54 | "@electron-forge/maker-zip": "^7.2.0", 55 | "@electron-forge/plugin-auto-unpack-natives": "^7.2.0", 56 | "@rabbitholesyndrome/electron-forge-maker-portable": "^0.2.0", 57 | "@types/dompurify": "^3.0.5", 58 | "@types/lodash": "^4.14.186", 59 | "@types/mockjs": "^1.0.7", 60 | "@types/nprogress": "^0.2.0", 61 | "@types/sortablejs": "^1.15.0", 62 | "@typescript-eslint/eslint-plugin": "^5.40.0", 63 | "@typescript-eslint/parser": "^5.40.0", 64 | "@vitejs/plugin-vue": "^3.1.2", 65 | "@vitejs/plugin-vue-jsx": "^2.0.1", 66 | "@vue/babel-plugin-jsx": "^1.1.1", 67 | "consola": "^2.15.3", 68 | "cross-env": "^7.0.3", 69 | "electron": "^28.2.1", 70 | "less": "^4.1.3", 71 | "mockjs": "^1.1.0", 72 | "postcss-html": "^1.5.0", 73 | "postcss-px-to-viewport": "^1.1.1", 74 | "prettier": "^2.7.1", 75 | "rollup": "^3.9.1", 76 | "rollup-plugin-visualizer": "^5.8.2", 77 | "typescript": "^4.8.4", 78 | "unplugin-auto-import": "^0.17.5", 79 | "unplugin-vue-components": "^0.26.0", 80 | "vite": "^3.2.5", 81 | "vite-plugin-compression": "^0.5.1", 82 | "vite-plugin-html-config": "^1.0.11", 83 | "vite-plugin-imagemin": "^0.6.1", 84 | "vite-svg-loader": "^3.6.0", 85 | "vue-tsc": "^1.0.14" 86 | }, 87 | "engines": { 88 | "node": ">=14.0.0" 89 | }, 90 | "resolutions": { 91 | "bin-wrapper": "npm:bin-wrapper-china", 92 | "rollup": "^2.56.3", 93 | "gifsicle": "5.2.0" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /public/1714006925783.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/1714006925783.jpg -------------------------------------------------------------------------------- /public/1722745910257.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/1722745910257.jpg -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | k5.vicicode.com -------------------------------------------------------------------------------- /public/K5Channel.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/K5Channel.xlsx -------------------------------------------------------------------------------- /public/K5Channel_EN.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/K5Channel_EN.xlsx -------------------------------------------------------------------------------- /public/LOSEHU117P6.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/LOSEHU117P6.bin -------------------------------------------------------------------------------- /public/LOSEHU117P6K.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/LOSEHU117P6K.bin -------------------------------------------------------------------------------- /public/LOSEHU126.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/LOSEHU126.bin -------------------------------------------------------------------------------- /public/LOSEHU126H.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/LOSEHU126H.bin -------------------------------------------------------------------------------- /public/LOSEHU126K.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/LOSEHU126K.bin -------------------------------------------------------------------------------- /public/L_BL001.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/L_BL001.bin -------------------------------------------------------------------------------- /public/O1CN019Pat6v1ZuNgxL8CRt_!!6000000003254-0-tps-800-450.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/O1CN019Pat6v1ZuNgxL8CRt_!!6000000003254-0-tps-800-450.jpg -------------------------------------------------------------------------------- /public/adimg1c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/adimg1c.png -------------------------------------------------------------------------------- /public/adimg2c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/adimg2c.png -------------------------------------------------------------------------------- /public/gy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/gy.png -------------------------------------------------------------------------------- /public/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/img1.png -------------------------------------------------------------------------------- /public/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/img2.png -------------------------------------------------------------------------------- /public/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/img3.png -------------------------------------------------------------------------------- /public/img4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/img4.png -------------------------------------------------------------------------------- /public/img5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/img5.png -------------------------------------------------------------------------------- /public/img6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/img6.png -------------------------------------------------------------------------------- /public/jjgg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/jjgg.jpg -------------------------------------------------------------------------------- /public/k5web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/k5web.png -------------------------------------------------------------------------------- /public/mm_facetoface_collect_qrcode_1714392837792.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/mm_facetoface_collect_qrcode_1714392837792.png -------------------------------------------------------------------------------- /public/new_font_h.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/new_font_h.bin -------------------------------------------------------------------------------- /public/new_font_k.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/new_font_k.bin -------------------------------------------------------------------------------- /public/new_font_k_f.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/new_font_k_f.bin -------------------------------------------------------------------------------- /public/old_font.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/old_font.bin -------------------------------------------------------------------------------- /public/pinyin.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/pinyin.bin -------------------------------------------------------------------------------- /public/pinyin_plus.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/pinyin_plus.bin -------------------------------------------------------------------------------- /public/qrcode_1714310463601.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/qrcode_1714310463601.jpg -------------------------------------------------------------------------------- /public/rhino-design-800x450.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/rhino-design-800x450.png -------------------------------------------------------------------------------- /public/sms_test.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/sms_test.bin -------------------------------------------------------------------------------- /public/ssb.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/public/ssb.bin -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 98 | -------------------------------------------------------------------------------- /src/api/interceptor.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import type { AxiosRequestConfig, AxiosResponse } from 'axios'; 3 | import { Message, Modal } from '@arco-design/web-vue'; 4 | import { useUserStore } from '@/store'; 5 | import { getToken } from '@/utils/auth'; 6 | 7 | export interface HttpResponse { 8 | status: number; 9 | msg: string; 10 | code: number; 11 | data: T; 12 | } 13 | 14 | if (import.meta.env.VITE_API_BASE_URL) { 15 | axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL; 16 | } 17 | 18 | axios.interceptors.request.use( 19 | (config: AxiosRequestConfig) => { 20 | // let each request carry token 21 | // this example using the JWT token 22 | // Authorization is a custom headers key 23 | // please modify it according to the actual situation 24 | const token = getToken(); 25 | if (token) { 26 | if (!config.headers) { 27 | config.headers = {}; 28 | } 29 | config.headers.Authorization = `Bearer ${token}`; 30 | } 31 | return config; 32 | }, 33 | (error) => { 34 | // do something 35 | return Promise.reject(error); 36 | } 37 | ); 38 | // add response interceptors 39 | axios.interceptors.response.use( 40 | (response: AxiosResponse) => { 41 | const res = response.data; 42 | // if the custom code is not 20000, it is judged as an error. 43 | if (res.code !== 200 && res.code !== 1) { 44 | Message.error({ 45 | content: res.msg || 'Error', 46 | duration: 5 * 1000, 47 | }); 48 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; 49 | if ( 50 | [50008, 50012, 50014].includes(res.code) && 51 | response.config.url !== '/api/user/info' 52 | ) { 53 | Modal.error({ 54 | title: 'Confirm logout', 55 | content: 56 | 'You have been logged out, you can cancel to stay on this page, or log in again', 57 | okText: 'Re-Login', 58 | async onOk() { 59 | const userStore = useUserStore(); 60 | 61 | await userStore.logout(); 62 | window.location.reload(); 63 | }, 64 | }); 65 | } 66 | return Promise.reject(new Error(res.msg || 'Error')); 67 | } 68 | return res; 69 | }, 70 | (error) => { 71 | Message.error({ 72 | content: error.msg || 'Request Error', 73 | duration: 5 * 1000, 74 | }); 75 | return Promise.reject(error); 76 | } 77 | ); 78 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import type { RouteRecordNormalized } from 'vue-router'; 3 | import { UserState } from '@/store/modules/user/types'; 4 | 5 | export interface LoginData { 6 | username: string; 7 | password: string; 8 | } 9 | 10 | export interface LoginRes { 11 | token: string; 12 | } 13 | export function login(data: LoginData) { 14 | return axios.post('/api/user/login', data); 15 | } 16 | 17 | export function logout() { 18 | return axios.post('/api/user/logout'); 19 | } 20 | 21 | export function getUserInfo() { 22 | return axios.post('/api/user/info'); 23 | } 24 | 25 | export function getMenuList() { 26 | return axios.post('/api/user/menu'); 27 | } 28 | -------------------------------------------------------------------------------- /src/assets/images/login-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/src/assets/images/login-banner.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/style/breakpoint.less: -------------------------------------------------------------------------------- 1 | // ==============breakpoint============ 2 | 3 | // Extra small screen / phone 4 | @screen-xs: 480px; 5 | 6 | // Small screen / tablet 7 | @screen-sm: 576px; 8 | 9 | // Medium screen / desktop 10 | @screen-md: 768px; 11 | 12 | // Large screen / wide desktop 13 | @screen-lg: 992px; 14 | 15 | // Extra large screen / full hd 16 | @screen-xl: 1200px; 17 | 18 | // Extra extra large screen / large desktop 19 | @screen-xxl: 1600px; 20 | -------------------------------------------------------------------------------- /src/assets/style/global.less: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | width: 100%; 8 | height: 100%; 9 | margin: 0; 10 | padding: 0; 11 | font-size: 14px; 12 | background-color: var(--color-bg-1); 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-font-smoothing: antialiased; 15 | } 16 | 17 | .echarts-tooltip-diy { 18 | background: linear-gradient( 19 | 304.17deg, 20 | rgba(253, 254, 255, 0.6) -6.04%, 21 | rgba(244, 247, 252, 0.6) 85.2% 22 | ) !important; 23 | border: none !important; 24 | backdrop-filter: blur(10px) !important; 25 | /* Note: backdrop-filter has minimal browser support */ 26 | 27 | border-radius: 6px !important; 28 | .content-panel { 29 | display: flex; 30 | justify-content: space-between; 31 | padding: 0 9px; 32 | background: rgba(255, 255, 255, 0.8); 33 | width: 164px; 34 | height: 32px; 35 | line-height: 32px; 36 | box-shadow: 6px 0px 20px rgba(34, 87, 188, 0.1); 37 | border-radius: 4px; 38 | margin-bottom: 4px; 39 | } 40 | .tooltip-title { 41 | margin: 0 0 10px 0; 42 | } 43 | p { 44 | margin: 0; 45 | } 46 | .tooltip-title, 47 | .tooltip-value { 48 | font-size: 13px; 49 | line-height: 15px; 50 | display: flex; 51 | align-items: center; 52 | text-align: right; 53 | color: #1d2129; 54 | font-weight: bold; 55 | } 56 | .tooltip-item-icon { 57 | display: inline-block; 58 | margin-right: 8px; 59 | width: 10px; 60 | height: 10px; 61 | border-radius: 50%; 62 | } 63 | } 64 | 65 | .general-card { 66 | border-radius: 4px; 67 | border: none; 68 | & > .arco-card-header { 69 | height: auto; 70 | padding: 20px; 71 | border: none; 72 | } 73 | & > .arco-card-body { 74 | padding: 0 20px 20px 20px; 75 | } 76 | } 77 | 78 | .split-line { 79 | border-color: rgb(var(--gray-2)); 80 | } 81 | 82 | .arco-table-cell { 83 | .circle { 84 | display: inline-block; 85 | margin-right: 4px; 86 | width: 6px; 87 | height: 6px; 88 | border-radius: 50%; 89 | background-color: rgb(var(--blue-6)); 90 | &.pass { 91 | background-color: rgb(var(--green-6)); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/components/breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | 25 | 36 | -------------------------------------------------------------------------------- /src/components/chart/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/components/footer/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 32 | -------------------------------------------------------------------------------- /src/components/global-setting/block.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 61 | 62 | 80 | -------------------------------------------------------------------------------- /src/components/global-setting/form-wrapper.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 40 | -------------------------------------------------------------------------------- /src/components/global-setting/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 86 | 87 | 99 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue'; 2 | import { use } from 'echarts/core'; 3 | import { CanvasRenderer } from 'echarts/renderers'; 4 | import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts'; 5 | import { 6 | GridComponent, 7 | TooltipComponent, 8 | LegendComponent, 9 | DataZoomComponent, 10 | GraphicComponent, 11 | } from 'echarts/components'; 12 | import Chart from './chart/index.vue'; 13 | import Breadcrumb from './breadcrumb/index.vue'; 14 | 15 | // Manually introduce ECharts modules to reduce packing size 16 | 17 | use([ 18 | CanvasRenderer, 19 | BarChart, 20 | LineChart, 21 | PieChart, 22 | RadarChart, 23 | GridComponent, 24 | TooltipComponent, 25 | LegendComponent, 26 | DataZoomComponent, 27 | GraphicComponent, 28 | ]); 29 | 30 | export default { 31 | install(Vue: App) { 32 | Vue.component('Chart', Chart); 33 | Vue.component('Breadcrumb', Breadcrumb); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/menu/index.vue: -------------------------------------------------------------------------------- 1 | 147 | 148 | 161 | -------------------------------------------------------------------------------- /src/components/menu/use-menu-tree.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue'; 2 | import { RouteRecordRaw, RouteRecordNormalized } from 'vue-router'; 3 | import usePermission from '@/hooks/permission'; 4 | import { useAppStore } from '@/store'; 5 | import appClientMenus from '@/router/app-menus'; 6 | import { cloneDeep } from 'lodash'; 7 | 8 | export default function useMenuTree() { 9 | const permission = usePermission(); 10 | const appStore = useAppStore(); 11 | const appRoute = computed(() => { 12 | if (appStore.menuFromServer) { 13 | return appStore.appAsyncMenus; 14 | } 15 | return appClientMenus; 16 | }); 17 | const menuTree = computed(() => { 18 | const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[]; 19 | copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => { 20 | return (a.meta.order || 0) - (b.meta.order || 0); 21 | }); 22 | function travel(_routes: RouteRecordRaw[], layer: number) { 23 | if (!_routes) return null; 24 | 25 | const collector: any = _routes.map((element) => { 26 | // no access 27 | if (!permission.accessRouter(element)) { 28 | return null; 29 | } 30 | 31 | // leaf node 32 | if (element.meta?.hideChildrenInMenu || !element.children) { 33 | element.children = []; 34 | return element; 35 | } 36 | 37 | // route filter hideInMenu true 38 | element.children = element.children.filter( 39 | (x) => x.meta?.hideInMenu !== true 40 | ); 41 | 42 | // Associated child node 43 | const subItem = travel(element.children, layer + 1); 44 | 45 | if (subItem.length) { 46 | element.children = subItem; 47 | return element; 48 | } 49 | // the else logic 50 | if (layer > 1) { 51 | element.children = subItem; 52 | return element; 53 | } 54 | 55 | if (element.meta?.hideInMenu === false) { 56 | return element; 57 | } 58 | 59 | return null; 60 | }); 61 | return collector.filter(Boolean); 62 | } 63 | return travel(copyRouter, 0); 64 | }); 65 | 66 | return { 67 | menuTree, 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/components/tab-bar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 61 | 62 | 102 | -------------------------------------------------------------------------------- /src/components/tab-bar/readme.md: -------------------------------------------------------------------------------- 1 | ## 组件说明 2 | 3 | 该组件非官方最终设计规范,以单独组件存在。 4 | 5 | 同时仅仅提供最基本的功能,后续进行优化及更改。 6 | 7 | 8 | ## Component description 9 | 10 | The component unofficial final design specification exists as a separate component. 11 | 12 | At the same time, only the most basic functions are provided, and subsequent optimizations and changes will be made. -------------------------------------------------------------------------------- /src/components/tab-bar/tab-item.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 169 | 170 | 201 | -------------------------------------------------------------------------------- /src/config/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": "light", 3 | "colorWeak": false, 4 | "navbar": true, 5 | "menu": true, 6 | "topMenu": false, 7 | "hideMenu": false, 8 | "menuCollapse": false, 9 | "footer": true, 10 | "themeColor": "#165DFF", 11 | "menuWidth": 220, 12 | "globalSettings": false, 13 | "device": "desktop", 14 | "tabBar": false, 15 | "menuFromServer": false, 16 | "serverMenu": [], 17 | "connectState": false, 18 | "firmwareVersion": "", 19 | "connectPort": null, 20 | "configuration": null 21 | } 22 | -------------------------------------------------------------------------------- /src/directive/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue'; 2 | import permission from './permission'; 3 | 4 | export default { 5 | install(Vue: App) { 6 | Vue.directive('permission', permission); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/directive/permission/index.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveBinding } from 'vue'; 2 | import { useUserStore } from '@/store'; 3 | 4 | function checkPermission(el: HTMLElement, binding: DirectiveBinding) { 5 | const { value } = binding; 6 | const userStore = useUserStore(); 7 | const { role } = userStore; 8 | 9 | if (Array.isArray(value)) { 10 | if (value.length > 0) { 11 | const permissionValues = value; 12 | 13 | const hasPermission = permissionValues.includes(role); 14 | if (!hasPermission && el.parentNode) { 15 | el.parentNode.removeChild(el); 16 | } 17 | } 18 | } else { 19 | throw new Error(`need roles! Like v-permission="['admin','user']"`); 20 | } 21 | } 22 | 23 | export default { 24 | mounted(el: HTMLElement, binding: DirectiveBinding) { 25 | checkPermission(el, binding); 26 | }, 27 | updated(el: HTMLElement, binding: DirectiveBinding) { 28 | checkPermission(el, binding); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/drivers/losehu117.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LoseHu 117 历史版本", 3 | "uart": "official", 4 | "charset": "official" 5 | } -------------------------------------------------------------------------------- /src/drivers/losehu117k.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LoseHu 117 历史版本扩容版", 3 | "uart": "losehu", 4 | "charset": "losehu", 5 | "K": true 6 | } -------------------------------------------------------------------------------- /src/drivers/losehu118.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LoseHu 118+", 3 | "uart": "official", 4 | "charset": "official" 5 | } -------------------------------------------------------------------------------- /src/drivers/losehu118h.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LoseHu 118+ 2Mbit 扩容版", 3 | "uart": "losehu", 4 | "charset": "gb2312", 5 | "H": true, 6 | "sat": true 7 | } 8 | -------------------------------------------------------------------------------- /src/drivers/losehu118k.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LoseHu 118+ 扩容版", 3 | "uart": "losehu", 4 | "charset": "gb2312", 5 | "K": true 6 | } -------------------------------------------------------------------------------- /src/drivers/losehu120k.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LoseHu 120+ 扩容版", 3 | "uart": "losehu", 4 | "charset": "gb2312", 5 | "K": true, 6 | "sat": true 7 | } -------------------------------------------------------------------------------- /src/drivers/losehu124h.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LoseHu 124+ 2Mbit 扩容版", 3 | "uart": "losehu", 4 | "charset": "gb2312", 5 | "H": true, 6 | "sat": true, 7 | "newpinyin": true 8 | } 9 | -------------------------------------------------------------------------------- /src/drivers/losehubl.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LoseHu 引导程序", 3 | "uart": "losehu", 4 | "charset": "gb2312", 5 | "H": true, 6 | "sat": true, 7 | "newpinyin": true 8 | } 9 | -------------------------------------------------------------------------------- /src/drivers/losehud.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LoseHu Doppler", 3 | "uart": "losehu", 4 | "charset": "gb2312", 5 | "H": true, 6 | "sat": true, 7 | "sat2": true, 8 | "newpinyin": true 9 | } 10 | -------------------------------------------------------------------------------- /src/drivers/lts.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LoseHu Patch LTS(BD8DFN)", 3 | "uart": "official", 4 | "charset": "official", 5 | "localmdc": true 6 | } -------------------------------------------------------------------------------- /src/drivers/ltsk.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LoseHu Patch LTS(BD8DFN)扩容版", 3 | "uart": "losehu", 4 | "charset": "losehu", 5 | "K": true, 6 | "localmdc": true, 7 | "fm30": true 8 | } -------------------------------------------------------------------------------- /src/drivers/todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Official" 3 | } -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue'; 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | interface ImportMetaEnv { 10 | readonly VITE_API_BASE_URL: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/chart-option.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue'; 2 | import { EChartsOption } from 'echarts'; 3 | import { useAppStore } from '@/store'; 4 | 5 | // for code hints 6 | // import { SeriesOption } from 'echarts'; 7 | // Because there are so many configuration items, this provides a relatively convenient code hint. 8 | // When using vue, pay attention to the reactive issues. It is necessary to ensure that corresponding functions can be triggered, TypeScript does not report errors, and code writing is convenient. 9 | interface optionsFn { 10 | (isDark: boolean): EChartsOption; 11 | } 12 | 13 | export default function useChartOption(sourceOption: optionsFn) { 14 | const appStore = useAppStore(); 15 | const isDark = computed(() => { 16 | return appStore.theme === 'dark'; 17 | }); 18 | // echarts support https://echarts.apache.org/zh/theme-builder.html 19 | // It's not used here 20 | // TODO echarts themes 21 | const chartOption = computed(() => { 22 | return sourceOption(isDark.value); 23 | }); 24 | return { 25 | chartOption, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/loading.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | 3 | export default function useLoading(initValue = false) { 4 | const loading = ref(initValue); 5 | const setLoading = (value: boolean) => { 6 | loading.value = value; 7 | }; 8 | const toggle = () => { 9 | loading.value = !loading.value; 10 | }; 11 | return { 12 | loading, 13 | setLoading, 14 | toggle, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/hooks/locale.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue'; 2 | import { useI18n } from 'vue-i18n'; 3 | import { Message } from '@arco-design/web-vue'; 4 | 5 | export default function useLocale() { 6 | const i18 = useI18n(); 7 | const currentLocale = computed(() => { 8 | return i18.locale.value; 9 | }); 10 | const changeLocale = (value: string) => { 11 | if (i18.locale.value === value) { 12 | return; 13 | } 14 | i18.locale.value = value; 15 | localStorage.setItem('arco-locale', value); 16 | Message.success(i18.t('navbar.action.locale')); 17 | }; 18 | return { 19 | currentLocale, 20 | changeLocale, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/permission.ts: -------------------------------------------------------------------------------- 1 | import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'; 2 | import { useUserStore } from '@/store'; 3 | 4 | export default function usePermission() { 5 | const userStore = useUserStore(); 6 | return { 7 | accessRouter(route: RouteLocationNormalized | RouteRecordRaw) { 8 | return ( 9 | !route.meta?.requiresAuth || 10 | !route.meta?.roles || 11 | route.meta?.roles?.includes('*') || 12 | route.meta?.roles?.includes(userStore.role) 13 | ); 14 | }, 15 | findFirstPermissionRoute(_routers: any, role = 'admin') { 16 | const cloneRouters = [..._routers]; 17 | while (cloneRouters.length) { 18 | const firstElement = cloneRouters.shift(); 19 | if ( 20 | firstElement?.meta?.roles?.find((el: string[]) => { 21 | return el.includes('*') || el.includes(role); 22 | }) 23 | ) 24 | return { name: firstElement.name }; 25 | if (firstElement?.children) { 26 | cloneRouters.push(...firstElement.children); 27 | } 28 | } 29 | return null; 30 | }, 31 | // You can add any rules you want 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/hooks/request.ts: -------------------------------------------------------------------------------- 1 | import { ref, UnwrapRef } from 'vue'; 2 | import { AxiosResponse } from 'axios'; 3 | import { HttpResponse } from '@/api/interceptor'; 4 | import useLoading from './loading'; 5 | 6 | // use to fetch list 7 | // Don't use async function. It doesn't work in async function. 8 | // Use the bind function to add parameters 9 | // example: useRequest(api.bind(null, {})) 10 | 11 | export default function useRequest( 12 | api: () => Promise>, 13 | defaultValue = [] as unknown as T, 14 | isLoading = true 15 | ) { 16 | const { loading, setLoading } = useLoading(isLoading); 17 | const response = ref(defaultValue); 18 | api() 19 | .then((res) => { 20 | response.value = res.data as unknown as UnwrapRef; 21 | }) 22 | .finally(() => { 23 | setLoading(false); 24 | }); 25 | return { loading, response }; 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/responsive.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onBeforeMount, onBeforeUnmount } from 'vue'; 2 | import { useDebounceFn } from '@vueuse/core'; 3 | import { useAppStore } from '@/store'; 4 | import { addEventListen, removeEventListen } from '@/utils/event'; 5 | 6 | const WIDTH = 992; // https://arco.design/vue/component/grid#responsivevalue 7 | 8 | function queryDevice() { 9 | const rect = document.body.getBoundingClientRect(); 10 | return rect.width - 1 < WIDTH; 11 | } 12 | 13 | export default function useResponsive(immediate?: boolean) { 14 | const appStore = useAppStore(); 15 | function resizeHandler() { 16 | if (!document.hidden) { 17 | const isMobile = queryDevice(); 18 | appStore.toggleDevice(isMobile ? 'mobile' : 'desktop'); 19 | appStore.toggleMenu(isMobile); 20 | } 21 | } 22 | const debounceFn = useDebounceFn(resizeHandler, 100); 23 | onMounted(() => { 24 | if (immediate) debounceFn(); 25 | }); 26 | onBeforeMount(() => { 27 | addEventListen(window, 'resize', debounceFn); 28 | }); 29 | onBeforeUnmount(() => { 30 | removeEventListen(window, 'resize', debounceFn); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/hooks/themes.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue'; 2 | import { useAppStore } from '@/store'; 3 | 4 | export default function useThemes() { 5 | const appStore = useAppStore(); 6 | const isDark = computed(() => { 7 | return appStore.theme === 'dark'; 8 | }); 9 | return { 10 | isDark, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/hooks/user.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'vue-router'; 2 | import { Message } from '@arco-design/web-vue'; 3 | 4 | import { useUserStore } from '@/store'; 5 | 6 | export default function useUser() { 7 | const router = useRouter(); 8 | const userStore = useUserStore(); 9 | const logout = async (logoutTo?: string) => { 10 | await userStore.logout(); 11 | const currentRoute = router.currentRoute.value; 12 | Message.success('登出成功'); 13 | router.push({ 14 | name: logoutTo && typeof logoutTo === 'string' ? logoutTo : 'login', 15 | query: { 16 | ...router.currentRoute.value.query, 17 | redirect: currentRoute.name as string, 18 | }, 19 | }); 20 | }; 21 | return { 22 | logout, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks/visible.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | 3 | export default function useVisible(initValue = false) { 4 | const visible = ref(initValue); 5 | const setVisible = (value: boolean) => { 6 | visible.value = value; 7 | }; 8 | const toggle = () => { 9 | visible.value = !visible.value; 10 | }; 11 | return { 12 | visible, 13 | setVisible, 14 | toggle, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/layout/page-layout.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | import localeWorkplace from '@/views/dashboard/workplace/locale/en-US'; 2 | 3 | import localeSearchTable from '@/views/list/search-table/locale/en-US'; 4 | import localeCardList from '@/views/list/card/locale/en-US'; 5 | 6 | import localeSettings from './en-US/settings'; 7 | 8 | export default { 9 | 'On': 'On', 10 | 'Off': 'Off', 11 | 'menu.dashboard': 'CPS', 12 | 'menu.cps.channel': 'Channel', 13 | 'menu.cps.settings': 'Settings', 14 | 'menu.server.dashboard': 'Dashboard-Server', 15 | 'menu.server.workplace': 'Workplace-Server', 16 | 'menu.server.monitor': 'Monitor-Server', 17 | 'menu.list': 'Tools', 18 | 'menu.result': 'Result', 19 | 'menu.exception': 'Exception', 20 | 'menu.form': 'Form', 21 | 'menu.profile': 'Profile', 22 | 'menu.visualization': 'Data Visualization', 23 | 'menu.user': 'User Center', 24 | 'menu.arcoWebsite': 'Arco Design', 25 | 'menu.faq': 'FAQ', 26 | 'navbar.docs': 'Docs', 27 | 'navbar.action.locale': 'Switch to English', 28 | 'navbar.author': 'Author: BD8DFN', 29 | 'navbar.connect': 'Connect', 30 | 'navbar.disconnect': 'Disconnect', 31 | 'navbar.qa': 'Feedback', 32 | 'global.8kb': '8KB (64Kbit)', 33 | 'global.128kb': '128KB (1Mbit)', 34 | 'global.256kb': '256KB (2Mbit)', 35 | 'global.384kb': '384KB (3Mbit)', 36 | 'global.512kb': '512KB (4Mbit)', 37 | 'menu.rb': 'Backup/Restore', 38 | 'menu.flash': 'Flasher', 39 | 'menu.image': 'Startup Image', 40 | 'menu.font': 'Set Patch', 41 | 'menu.satellite': 'Satcom', 42 | 'global.onStart': ' (Put the UV-K5 into normal mode)', 43 | 'global.onBoot': ' (Put the UV-K5 into programming mode)', 44 | 'cps.onDeviceRead': 'Read from device', 45 | 'cps.onDeviceWrite': 'Write to device', 46 | 'cps.downloadImportTemplate': 'Download Import Templates', 47 | 'cps.import': 'Import', 48 | 'cps.export': 'Export', 49 | 'cps.save': 'Save', 50 | 'cps.load': 'Load', 51 | 'cps.line1': 'First line of text on startup screen', 52 | 'cps.line2': 'Second line of text on startup screen', 53 | 'cps.mdclocplay': 'Local MDC Play (Only support my firmware)', 54 | 'cps.sort': 'Sort', 55 | 'cps.name': 'Name', 56 | 'cps.bandwidth': 'Bandwidth', 57 | 'cps.tx': 'TX Frequency', 58 | 'cps.rx': 'RX Frequency', 59 | 'cps.power': 'Power', 60 | 'cps.rxToneType': 'RX Tone Type', 61 | 'cps.rxToneCTCSS': 'RX CTCSS(Hz)', 62 | 'cps.rxToneDCS': 'RX DCS', 63 | 'cps.txToneType': 'TX Tone Type', 64 | 'cps.txToneCTCSS': 'TX CTCSS(Hz)', 65 | 'cps.txToneDCS': 'TX DCS', 66 | 'cps.step': 'Frequency Step', 67 | 'cps.reverse': 'Reverse', 68 | 'cps.scramb': 'Scramb', 69 | 'cps.busy': 'Busy', 70 | 'cps.pttid': 'PTTID', 71 | 'cps.mode': 'Mode', 72 | 'cps.dtmf': 'DTMF Decode', 73 | 'cps.scanlist': 'Scanlist', 74 | 'cps.operate': 'Operate', 75 | 'cps.clear': 'Clear', 76 | 'tool.quickbackup': 'Quick Backup', 77 | 'tool.fullbackup': 'Full Backup', 78 | 'tool.cleardata': 'Clear EEPROM', 79 | 'tool.backupConfig': 'Backup Config', 80 | 'tool.restoreConfig': 'Restore Config', 81 | 'tool.backupCalibration': 'Backup Calibration', 82 | 'tool.restoreCalibration': 'Restore Calibration', 83 | 'tool.backup': 'Backup', 84 | 'tool.restore': 'Restore', 85 | 'tool.autocheck': 'AUTO', 86 | 'tool.selectSize': 'Select EEPROM size', 87 | 'tool.first': 'Warning ', 88 | 'tool.firstTitle': '', 89 | 'tool.last': '(LAST WARNING)', 90 | 'tool.clearMessage': 'This will clear the EEPROM of all contents, including configuration and calibration data!!!!', 91 | 'tool.selectFirmware': 'Select Firmware', 92 | 'tool.flash': 'FLASH', 93 | 'tool.selectImage': 'Select Image', 94 | 'tool.write': 'Write to device', 95 | 'tool.fontwrite': 'LOSEHU Chinese Character Set Write', 96 | 'tool.pinyinwrite': 'LOSEHU H Chinese Pinyin Set Write', 97 | 'tool.writefontwrite': 'Character Set Write', 98 | 'tool.Simplified_Chinese': 'CHS', 99 | 'tool.Traditional_Chinese': 'CHT', 100 | 'tool.writepinyin': 'Pinyin Set Write', 101 | 'tool.brtime': 'Browser Time', 102 | 'tool.selectSatellite': 'Select satellite', 103 | 'tool.longitude': 'Longitude', 104 | 'tool.latitude': 'Latitude', 105 | 'tool.altitude': 'Altitude', 106 | 'tool.brlonlat': 'Get browser location', 107 | 'tool.phonelonlat': 'Get phone location', 108 | 'tool.satpasstime': 'Get satellite pass time', 109 | 'tool.selectPassTime': 'Select pass time', 110 | 'tool.txFreq': 'TX Frequency', 111 | 'tool.txTone': 'TX Tone', 112 | 'tool.rxFreq': 'RX Frequency', 113 | 'tool.rxTone': 'RX Tone', 114 | 'tool.writeData': 'Write to device', 115 | 'tool.off': 'Off', 116 | 'tool.scanqr': 'Scan QR Code', 117 | 'tool.scannotice': 'Uploaded location information will be cached by the server for 10 minutes', 118 | 'tool.scaned': 'Scanned and uploaded', 119 | 'global.nosupport': 'Current browser does not support WebSerial function, please use Chrome, Edge, Opera browser.', 120 | 'global.connectFail': 'Connect Failure', 121 | 'global.handshakeFail': 'Handshake Failure', 122 | 'menu.workshop': 'Workshop', 123 | 'menu.firmware': 'Firmware Store', 124 | 'menu.channel': 'Channel Share', 125 | 'global.use': 'Use', 126 | 'global.download': 'Download', 127 | 'tool.ssbpatch': 'LOSEHU S Firmware SI4732 SSB Patch', 128 | 'tool.writessbpatch': 'SSB Patch Write', 129 | 'global.login': 'Login', 130 | 'global.register': 'Register', 131 | 'global.motto': 'Motto', 132 | 'global.logout': 'Logout', 133 | 'global.username': 'Username', 134 | 'global.nickname': 'Nickname', 135 | 'global.password': 'Password', 136 | 'global.password2': 'Retype password ', 137 | 'image.negative': 'Negative', 138 | 'workplace.clickNotice': ' (Official firmware can only detect 8KB/64Kbit)', 139 | 'menu.cps.radio': 'Radio', 140 | 'menu.cps.mdc': 'MDC Contact', 141 | 'menu.cps.dtmf': 'DTMF Contact', 142 | 'cps.contact': 'Name', 143 | 'cps.mdcid': 'MDC ID', 144 | 'cps.dtmf.up': 'DTMF Up Code', 145 | 'cps.dtmf.down': 'DTMF Down Code', 146 | 'idea.diy': 'LOSEHU DIY', 147 | 'diy.generate': 'Generate', 148 | 'cps.dtmfid': 'DTMF ID', 149 | 'global.upload': 'Upload', 150 | 'global.loginUpload': '(Login to upload and share)', 151 | 'bl': 'Multi-booting', 152 | 'cs': 'Coming Soon', 153 | 'oi': 'Operating Instructions: ', 154 | 'bl.warning': '⚠: Experimental feature Use may damage radio station', 155 | 'bl.readme': 'Readme: ', 156 | 'bl.clear': 'Clear', 157 | 'bl.onlyEnglish': 'Firmware names are supported in English only', 158 | 'bl.drag': 'Select the firmware and drag the firmware card to the EEPROM grid', 159 | 'bl.bootloader': 'Bootloader Use', 160 | 'sat.selfSatInfo': 'My satellite parameters', 161 | 'sat.addSelfSat': 'Add my satellite', 162 | 'chat': 'Radio Chat', 163 | 'menu.cps.writeNoticeTitle': 'Confirm', 164 | 'menu.cps.writeNoticeContent': "Confirmation to write the channel shown on the web page to the device? (will override the device's current channel configuration)", 165 | 'menu.satellite2': 'Satcom 2.0', 166 | ...localeSettings, 167 | ...localeWorkplace, 168 | 169 | ...localeSearchTable, 170 | ...localeCardList, 171 | }; 172 | -------------------------------------------------------------------------------- /src/locale/en-US/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'settings.title': 'Settings', 3 | 'settings.themeColor': 'Theme Color', 4 | 'settings.content': 'Content Setting', 5 | 'settings.search': 'Search', 6 | 'settings.language': 'Language', 7 | 'settings.navbar': 'Navbar', 8 | 'settings.menuWidth': 'Menu Width (px)', 9 | 'settings.navbar.theme.toLight': 'Click to use light mode', 10 | 'settings.navbar.theme.toDark': 'Click to use dark mode', 11 | 'settings.navbar.screen.toFull': 'Click to switch to full screen mode', 12 | 'settings.navbar.screen.toExit': 'Click to exit the full screen mode', 13 | 'settings.navbar.alerts': 'alerts', 14 | 'settings.menu': 'Menu', 15 | 'settings.topMenu': 'Top Menu', 16 | 'settings.tabBar': 'Tab Bar', 17 | 'settings.footer': 'Footer', 18 | 'settings.otherSettings': 'Other Settings', 19 | 'settings.colorWeak': 'Color Weak', 20 | 'settings.alertContent': 21 | 'After the configuration is only temporarily effective, if you want to really affect the project, click the "Copy Settings" button below and replace the configuration in settings.json.', 22 | 'settings.copySettings': 'Copy Settings', 23 | 'settings.copySettings.message': 24 | 'Copy succeeded, please paste to file src/settings.json.', 25 | 'settings.close': 'Close', 26 | 'settings.color.tooltip': 27 | '10 gradient colors generated according to the theme color', 28 | 'settings.menuFromServer': 'Menu From Server', 29 | }; 30 | -------------------------------------------------------------------------------- /src/locale/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n'; 2 | import en from './en-US'; 3 | import cn from './zh-CN'; 4 | 5 | export const LOCALE_OPTIONS = [ 6 | { label: '中文', value: 'zh-CN' }, 7 | { label: 'English', value: 'en-US' }, 8 | ]; 9 | const defaultLocale = localStorage.getItem('arco-locale') || navigator.language || 'en-US'; 10 | 11 | const i18n = createI18n({ 12 | locale: defaultLocale, 13 | fallbackLocale: 'en-US', 14 | legacy: false, 15 | allowComposition: true, 16 | messages: { 17 | 'en-US': en, 18 | 'zh-CN': cn, 19 | }, 20 | }); 21 | 22 | export default i18n; 23 | -------------------------------------------------------------------------------- /src/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import localeWorkplace from '@/views/dashboard/workplace/locale/zh-CN'; 2 | 3 | import localeSearchTable from '@/views/list/search-table/locale/zh-CN'; 4 | import localeCardList from '@/views/list/card/locale/zh-CN'; 5 | 6 | import localeSettings from './zh-CN/settings'; 7 | 8 | export default { 9 | 'On': '开', 10 | 'Off': '关', 11 | 'menu.dashboard': '写频', 12 | 'menu.cps.channel': '信道管理', 13 | 'menu.cps.settings': '设置管理', 14 | 'menu.server.dashboard': '仪表盘-服务端', 15 | 'menu.server.workplace': '工作台-服务端', 16 | 'menu.server.monitor': '实时监控-服务端', 17 | 'menu.list': '小工具', 18 | 'menu.result': '结果页', 19 | 'menu.exception': '异常页', 20 | 'menu.form': '表单页', 21 | 'menu.profile': '详情页', 22 | 'menu.visualization': '固件更新', 23 | 'menu.user': '个人中心', 24 | 'menu.arcoWebsite': 'Arco Design', 25 | 'menu.faq': '常见问题', 26 | 'navbar.docs': '文档中心', 27 | 'navbar.action.locale': '切换为中文', 28 | 'navbar.author': '作者:BD8DFN', 29 | 'navbar.connect': '连接', 30 | 'navbar.disconnect': '断开', 31 | 'navbar.qa': '问题反馈', 32 | 'global.8kb': '8KB(64Kbit)', 33 | 'global.128kb': '128KB(1Mbit)', 34 | 'global.256kb': '256KB(2Mbit)', 35 | 'global.384kb': '384KB(3Mbit)', 36 | 'global.512kb': '512KB(4Mbit)', 37 | 'menu.rb': '备份/还原', 38 | 'menu.flash': '固件升级', 39 | 'menu.image': '开机图片', 40 | 'menu.font': '字库写入', 41 | 'menu.satellite': '星历写入', 42 | 'global.onStart': '(手台应在开机状态下)', 43 | 'global.onBoot': '(手台应在刷机模式下)', 44 | 'cps.onDeviceRead': '从设备读取', 45 | 'cps.onDeviceWrite': '写入设备', 46 | 'cps.downloadImportTemplate': '下载导入模板', 47 | 'cps.import': '导入', 48 | 'cps.export': '导出', 49 | 'cps.save': '保存', 50 | 'cps.load': '加载', 51 | 'cps.line1': '启动画面首行文字', 52 | 'cps.line2': '启动画面次行文字', 53 | 'cps.mdclocplay': '本地播放首尾音(仅117P6)', 54 | 'cps.sort': '排序', 55 | 'cps.name': '信道名称', 56 | 'cps.bandwidth': '带宽', 57 | 'cps.tx': '发送频率', 58 | 'cps.rx': '接收频率', 59 | 'cps.power': '发送功率', 60 | 'cps.rxToneType': '接收亚音类型', 61 | 'cps.rxToneCTCSS': '接收亚音频(Hz)', 62 | 'cps.rxToneDCS': '接收亚音数码', 63 | 'cps.txToneType': '发送亚音类型', 64 | 'cps.txToneCTCSS': '发送亚音频(Hz)', 65 | 'cps.txToneDCS': '发送亚音数码', 66 | 'cps.step': '频率步进', 67 | 'cps.reverse': '倒频', 68 | 'cps.scramb': '加密', 69 | 'cps.busy': '繁忙禁发', 70 | 'cps.pttid': '信令码', 71 | 'cps.mode': '信道模式', 72 | 'cps.dtmf': 'DTMF解码', 73 | 'cps.scanlist': '扫描列表', 74 | 'cps.operate': '操作', 75 | 'cps.clear': '清空', 76 | 'tool.quickbackup': '快捷备份', 77 | 'tool.fullbackup': '完整备份', 78 | 'tool.cleardata': '清空数据', 79 | 'tool.backupConfig': '备份配置', 80 | 'tool.restoreConfig': '恢复配置', 81 | 'tool.backupCalibration': '备份校准', 82 | 'tool.restoreCalibration': '恢复校准', 83 | 'tool.backup': '备份', 84 | 'tool.restore': '恢复', 85 | 'tool.autocheck': '自动检测', 86 | 'tool.selectSize': '选择 EEPROM 大小', 87 | 'tool.first': '第 ', 88 | 'tool.firstTitle': ' 次警告', 89 | 'tool.last': '(最后警告)', 90 | 'tool.clearMessage': '这将会清空 EEPROM 所有内容,包括配置及校准数据!!!', 91 | 'tool.selectFirmware': '选择固件', 92 | 'tool.flash': '更新', 93 | 'tool.selectImage': '选择图片', 94 | 'tool.write': '写入', 95 | 'tool.fontwrite': 'LOSEHU 固件字库写入', 96 | 'tool.pinyinwrite': 'LOSEHU H 版固件拼音索引表', 97 | 'tool.writefontwrite': '自动写入字库', 98 | 'tool.Simplified_Chinese': '简体', 99 | 'tool.Traditional_Chinese': '繁体', 100 | 'tool.writepinyin': '写入拼音检索表', 101 | 'tool.brtime': '浏览器时间', 102 | 'tool.selectSatellite': '选择卫星', 103 | 'tool.longitude': '经度', 104 | 'tool.latitude': '纬度', 105 | 'tool.altitude': '海拔', 106 | 'tool.brlonlat': '浏览器获取经纬度', 107 | 'tool.phonelonlat': '手机扫码获取经纬度', 108 | 'tool.satpasstime': '获取卫星过境时间', 109 | 'tool.selectPassTime': '选择过境时间', 110 | 'tool.txFreq': '上行频率', 111 | 'tool.txTone': '上行亚音', 112 | 'tool.rxFreq': '下行频率', 113 | 'tool.rxTone': '下行亚音', 114 | 'tool.writeData': '写入数据', 115 | 'tool.off': '关闭', 116 | 'tool.scanqr': '手机扫码获取经纬度', 117 | 'tool.scannotice': '上传经纬度信息将被服务器缓存十分钟', 118 | 'tool.scaned': '已扫码上传', 119 | 'global.nosupport': '当前浏览器不支持网页串口功能,请使用 Chrome, Edge, Opera 浏览器。', 120 | 'global.connectFail': '连接失败', 121 | 'global.handshakeFail': '握手失败', 122 | 'menu.workshop': '创意工坊', 123 | 'menu.firmware': '固件市场', 124 | 'menu.channel': '信道分享', 125 | 'global.use': '使用', 126 | 'global.download': '下载', 127 | 'tool.ssbpatch': 'LOSEHU S 版固件 SI4732 单边带补丁', 128 | 'tool.writessbpatch': '写入单边带补丁', 129 | 'global.login': '登录', 130 | 'global.register': '注册', 131 | 'global.motto': '联系方式(用于找回密码)', 132 | 'global.logout': '退出', 133 | 'global.username': '*请输入用户名', 134 | 'global.nickname': '*请输入昵称', 135 | 'global.password': '*请输入密码', 136 | 'global.password2': '*请再次输入密码', 137 | 'image.negative': '反色', 138 | 'workplace.clickNotice': '(官方固件只能检测 8KB/64Kbit)', 139 | 'menu.cps.radio': '收音机', 140 | 'menu.cps.mdc': 'MDC 联系人', 141 | 'menu.cps.dtmf': 'DTMF 联系人', 142 | 'cps.contact': '联系人', 143 | 'cps.mdcid': 'MDC ID', 144 | 'idea.diy': '自定义萝卜固件', 145 | 'diy.generate': '生成', 146 | 'cps.dtmfid': 'DTMF ID', 147 | 'cps.dtmf.up': 'DTMF 上线码', 148 | 'cps.dtmf.down': 'DTMF 下线码', 149 | 'global.upload': '上传', 150 | 'global.loginUpload': '(登录可上传分享)', 151 | 'bl': '多系统', 152 | 'cs': '敬请期待', 153 | 'oi': '操作说明:', 154 | 'bl.warning': '⚠:实验性功能 使用可能会损坏手台', 155 | 'bl.readme': '使用说明:', 156 | 'bl.clear': '清空', 157 | 'bl.onlyEnglish': '固件名称仅支持英文', 158 | 'bl.drag': '选择固件后将固件卡片拖拽到上方 EEPROM', 159 | 'bl.bootloader': '引导程序占用区', 160 | 'sat.selfSatInfo': '我的卫星参数', 161 | 'sat.addSelfSat': '添加我的卫星', 162 | 'chat': '无线电聊天', 163 | 'menu.cps.writeNoticeTitle': '确认', 164 | 'menu.cps.writeNoticeContent': '确认将网页显示的信道写入设备吗?(将覆盖设备当前信道配置)', 165 | 'menu.satellite2': '星历写入 2.0', 166 | ...localeSettings, 167 | ...localeWorkplace, 168 | 169 | ...localeSearchTable, 170 | ...localeCardList, 171 | }; 172 | -------------------------------------------------------------------------------- /src/locale/zh-CN/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'settings.title': '页面配置', 3 | 'settings.themeColor': '主题色', 4 | 'settings.content': '内容区域', 5 | 'settings.search': '搜索', 6 | 'settings.language': '语言', 7 | 'settings.navbar': '导航栏', 8 | 'settings.menuWidth': '菜单宽度 (px)', 9 | 'settings.navbar.theme.toLight': '点击切换为亮色模式', 10 | 'settings.navbar.theme.toDark': '点击切换为暗黑模式', 11 | 'settings.navbar.screen.toFull': '点击切换全屏模式', 12 | 'settings.navbar.screen.toExit': '点击退出全屏模式', 13 | 'settings.navbar.alerts': '消息通知', 14 | 'settings.menu': '菜单栏', 15 | 'settings.topMenu': '顶部菜单栏', 16 | 'settings.tabBar': '多页签', 17 | 'settings.footer': '底部', 18 | 'settings.otherSettings': '其他设置', 19 | 'settings.colorWeak': '色弱模式', 20 | 'settings.alertContent': 21 | '配置之后仅是临时生效,要想真正作用于项目,点击下方的 "复制配置" 按钮,将配置替换到 settings.json 中即可。', 22 | 'settings.copySettings': '复制配置', 23 | 'settings.copySettings.message': 24 | '复制成功,请粘贴到 src/settings.json 文件中', 25 | 'settings.close': '关闭', 26 | 'settings.color.tooltip': 27 | '根据主题颜色生成的 10 个梯度色(将配置复制到项目中,主题色才能对亮色 / 暗黑模式同时生效)', 28 | 'settings.menuFromServer': '菜单来源于后台', 29 | }; 30 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import ArcoVueIcon from '@arco-design/web-vue/es/icon'; 3 | import globalComponents from '@/components'; 4 | import router from './router'; 5 | import store from './store'; 6 | import i18n from './locale'; 7 | import directive from './directive'; 8 | import App from './App.vue'; 9 | // Styles are imported via arco-plugin. See config/plugin/arcoStyleImport.ts in the directory for details 10 | // 样式通过 arco-plugin 插件导入。详见目录文件 config/plugin/arcoStyleImport.ts 11 | // https://arco.design/docs/designlab/use-theme-package 12 | import '@/assets/style/global.less'; 13 | import '@/api/interceptor'; 14 | import 'tdesign-vue-next/es/style/index.css'; 15 | import Updater from "./utils/AutoUpdate.js"; 16 | import VueMatomo from 'vue-matomo'; 17 | 18 | const AutoUpdate = new Updater() 19 | AutoUpdate.on('update',()=>{ 20 | setTimeout(async()=>{ 21 | if(process.env.NODE_ENV == 'development'){ 22 | return 23 | } 24 | const result = confirm('当前网站有更新,请点击确定刷新页面体验'); 25 | if(result){ 26 | location.reload(); 27 | } 28 | },500) 29 | }) 30 | 31 | const app = createApp(App); 32 | 33 | app.use(ArcoVueIcon); 34 | 35 | app.use(router); 36 | app.use(store); 37 | app.use(i18n); 38 | app.use(globalComponents); 39 | app.use(directive); 40 | 41 | if(import.meta.env.VITE_METER_SITE.split(',').indexOf(location.hostname) !== -1){ 42 | app.use(VueMatomo, { 43 | host: '//analytics.vicicode.com', 44 | siteId: 2, 45 | router: router 46 | }) 47 | } 48 | 49 | app.mount('#app'); 50 | -------------------------------------------------------------------------------- /src/router/app-menus/index.ts: -------------------------------------------------------------------------------- 1 | import { appRoutes, appExternalRoutes } from '../routes'; 2 | 3 | const mixinRoutes = [...appRoutes, ...appExternalRoutes]; 4 | 5 | const appClientMenus = mixinRoutes.map((el) => { 6 | const { name, path, meta, redirect, children } = el; 7 | return { 8 | name, 9 | path, 10 | meta, 11 | redirect, 12 | children, 13 | }; 14 | }); 15 | 16 | export default appClientMenus; 17 | -------------------------------------------------------------------------------- /src/router/constants.ts: -------------------------------------------------------------------------------- 1 | export const WHITE_LIST = [ 2 | { name: 'notFound', children: [] }, 3 | { name: 'login', children: [] }, 4 | ]; 5 | 6 | export const NOT_FOUND = { 7 | name: 'notFound', 8 | }; 9 | 10 | export const REDIRECT_ROUTE_NAME = 'Redirect'; 11 | 12 | export const DEFAULT_ROUTE_NAME = 'Workplace'; 13 | 14 | export const DEFAULT_ROUTE = { 15 | title: 'menu.dashboard.workplace', 16 | name: DEFAULT_ROUTE_NAME, 17 | fullPath: '/chirp/base', 18 | }; 19 | -------------------------------------------------------------------------------- /src/router/guard/index.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router'; 2 | import { setRouteEmitter } from '@/utils/route-listener'; 3 | import setupUserLoginInfoGuard from './userLoginInfo'; 4 | import setupPermissionGuard from './permission'; 5 | 6 | function setupPageGuard(router: Router) { 7 | router.beforeEach(async (to) => { 8 | // emit route change 9 | setRouteEmitter(to); 10 | }); 11 | } 12 | 13 | export default function createRouteGuard(router: Router) { 14 | setupPageGuard(router); 15 | setupUserLoginInfoGuard(router); 16 | setupPermissionGuard(router); 17 | } 18 | -------------------------------------------------------------------------------- /src/router/guard/permission.ts: -------------------------------------------------------------------------------- 1 | import type { Router, RouteRecordNormalized } from 'vue-router'; 2 | import NProgress from 'nprogress'; // progress bar 3 | 4 | import usePermission from '@/hooks/permission'; 5 | import { useUserStore, useAppStore } from '@/store'; 6 | import { appRoutes } from '../routes'; 7 | import { WHITE_LIST, NOT_FOUND } from '../constants'; 8 | 9 | export default function setupPermissionGuard(router: Router) { 10 | router.beforeEach(async (to, from, next) => { 11 | const appStore = useAppStore(); 12 | const userStore = useUserStore(); 13 | const Permission = usePermission(); 14 | const permissionsAllow = Permission.accessRouter(to); 15 | if (appStore.menuFromServer) { 16 | // 针对来自服务端的菜单配置进行处理 17 | // Handle routing configuration from the server 18 | 19 | // 根据需要自行完善来源于服务端的菜单配置的permission逻辑 20 | // Refine the permission logic from the server's menu configuration as needed 21 | if ( 22 | !appStore.appAsyncMenus.length && 23 | !WHITE_LIST.find((el) => el.name === to.name) 24 | ) { 25 | await appStore.fetchServerMenuConfig(); 26 | } 27 | const serverMenuConfig = [...appStore.appAsyncMenus, ...WHITE_LIST]; 28 | 29 | let exist = false; 30 | while (serverMenuConfig.length && !exist) { 31 | const element = serverMenuConfig.shift(); 32 | if (element?.name === to.name) exist = true; 33 | 34 | if (element?.children) { 35 | serverMenuConfig.push( 36 | ...(element.children as unknown as RouteRecordNormalized[]) 37 | ); 38 | } 39 | } 40 | if (exist && permissionsAllow) { 41 | next(); 42 | } else next(NOT_FOUND); 43 | } else { 44 | // eslint-disable-next-line no-lonely-if 45 | if (permissionsAllow) next(); 46 | else { 47 | const destination = 48 | Permission.findFirstPermissionRoute(appRoutes, userStore.role) || 49 | NOT_FOUND; 50 | next(destination); 51 | } 52 | } 53 | NProgress.done(); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /src/router/guard/userLoginInfo.ts: -------------------------------------------------------------------------------- 1 | import type { Router, LocationQueryRaw } from 'vue-router'; 2 | import NProgress from 'nprogress'; // progress bar 3 | 4 | export default function setupUserLoginInfoGuard(router: Router) { 5 | router.beforeEach(async (to, from, next) => { 6 | NProgress.start(); 7 | next(); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router'; 2 | import NProgress from 'nprogress'; // progress bar 3 | import 'nprogress/nprogress.css'; 4 | 5 | import { appRoutes } from './routes'; 6 | import { REDIRECT_MAIN, NOT_FOUND_ROUTE, SATLOC } from './routes/base'; 7 | import createRouteGuard from './guard'; 8 | 9 | NProgress.configure({ showSpinner: false }); // NProgress Configuration 10 | 11 | const router = createRouter({ 12 | history: createWebHashHistory(), 13 | routes: [ 14 | { 15 | path: '/', 16 | redirect: 'chirp/base', 17 | }, 18 | ...appRoutes, 19 | REDIRECT_MAIN, 20 | SATLOC, 21 | NOT_FOUND_ROUTE, 22 | ], 23 | scrollBehavior() { 24 | return { top: 0 }; 25 | }, 26 | }); 27 | 28 | createRouteGuard(router); 29 | 30 | export default router; 31 | -------------------------------------------------------------------------------- /src/router/routes/base.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router'; 2 | import { REDIRECT_ROUTE_NAME } from '@/router/constants'; 3 | 4 | export const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue'); 5 | 6 | export const REDIRECT_MAIN: RouteRecordRaw = { 7 | path: '/redirect', 8 | name: 'redirectWrapper', 9 | component: DEFAULT_LAYOUT, 10 | meta: { 11 | requiresAuth: true, 12 | hideInMenu: true, 13 | }, 14 | children: [ 15 | { 16 | path: '/redirect/:path', 17 | name: REDIRECT_ROUTE_NAME, 18 | component: () => import('@/views/redirect/index.vue'), 19 | meta: { 20 | requiresAuth: true, 21 | hideInMenu: true, 22 | }, 23 | }, 24 | ], 25 | }; 26 | 27 | export const SATLOC: RouteRecordRaw = { 28 | path: '/satloc', 29 | name: 'satloc', 30 | component: () => import('@/views/list/satloc/index.vue'), 31 | } 32 | 33 | export const NOT_FOUND_ROUTE: RouteRecordRaw = { 34 | path: '/:pathMatch(.*)*', 35 | name: 'notFound', 36 | component: () => import('@/views/not-found/index.vue'), 37 | }; 38 | -------------------------------------------------------------------------------- /src/router/routes/externalModules/faq.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | path: 'https://www.vicicode.com/', 3 | name: 'BD8DFN', 4 | meta: { 5 | locale: 'navbar.author', 6 | requiresAuth: true, 7 | order: 8, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordNormalized } from 'vue-router'; 2 | 3 | const modules = import.meta.glob('./modules/*.ts', { eager: true }); 4 | const externalModules = import.meta.glob('./externalModules/*.ts', { 5 | eager: true, 6 | }); 7 | 8 | function formatModules(_modules: any, result: RouteRecordNormalized[]) { 9 | Object.keys(_modules).forEach((key) => { 10 | const defaultModule = _modules[key].default; 11 | if (!defaultModule) return; 12 | const moduleList = Array.isArray(defaultModule) 13 | ? [...defaultModule] 14 | : [defaultModule]; 15 | result.push(...moduleList); 16 | }); 17 | return result; 18 | } 19 | 20 | export const appRoutes: RouteRecordNormalized[] = formatModules(modules, []); 21 | 22 | export const appExternalRoutes: RouteRecordNormalized[] = formatModules( 23 | externalModules, 24 | [] 25 | ); 26 | -------------------------------------------------------------------------------- /src/router/routes/modules/dashboard.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LAYOUT } from '../base'; 2 | import { AppRouteRecordRaw } from '../types'; 3 | 4 | const DASHBOARD: AppRouteRecordRaw = { 5 | path: '/chirp', 6 | name: 'dashboard', 7 | component: DEFAULT_LAYOUT, 8 | meta: { 9 | locale: 'menu.dashboard', 10 | requiresAuth: true, 11 | icon: 'icon-dashboard', 12 | order: 0, 13 | }, 14 | children: [ 15 | { 16 | path: 'base', 17 | name: 'Workplace', 18 | component: () => import('@/views/dashboard/workplace/index.vue'), 19 | meta: { 20 | locale: 'menu.dashboard.workplace', 21 | requiresAuth: true, 22 | roles: ['*'], 23 | }, 24 | }, 25 | { 26 | path: 'channel', 27 | name: 'Channel', 28 | component: () => import('@/views/list/search-table/index.vue'), 29 | meta: { 30 | locale: 'menu.cps.channel', 31 | requiresAuth: true, 32 | roles: ['*'], 33 | }, 34 | }, 35 | { 36 | path: 'radio', 37 | name: 'Radio', 38 | component: () => import('@/views/list/radio/index.vue'), 39 | meta: { 40 | locale: 'menu.cps.radio', 41 | requiresAuth: true, 42 | roles: ['*'], 43 | }, 44 | }, 45 | { 46 | path: 'mdc', 47 | name: 'Mdc', 48 | component: () => import('@/views/list/mdc/index.vue'), 49 | meta: { 50 | locale: 'menu.cps.mdc', 51 | requiresAuth: true, 52 | roles: ['*'], 53 | }, 54 | }, 55 | { 56 | path: 'dtmf', 57 | name: 'Dtmf', 58 | component: () => import('@/views/list/dtmf/index.vue'), 59 | meta: { 60 | locale: 'menu.cps.dtmf', 61 | requiresAuth: true, 62 | roles: ['*'], 63 | }, 64 | }, 65 | { 66 | path: 'settings', 67 | name: 'Settings', 68 | component: () => import('@/views/list/settings/index.vue'), 69 | meta: { 70 | locale: 'menu.cps.settings', 71 | requiresAuth: true, 72 | roles: ['*'], 73 | }, 74 | }, 75 | { 76 | path: 'thanks', 77 | name: 'Thanks', 78 | component: () => import('@/views/thanks/index.vue'), 79 | meta: { 80 | hideInMenu: true, 81 | locale: '感谢列表', 82 | requiresAuth: true, 83 | roles: ['*'], 84 | }, 85 | } 86 | ], 87 | }; 88 | 89 | export default DASHBOARD; 90 | -------------------------------------------------------------------------------- /src/router/routes/modules/idea.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LAYOUT } from '../base'; 2 | import { AppRouteRecordRaw } from '../types'; 3 | 4 | const IDEA: AppRouteRecordRaw = { 5 | path: '/idea', 6 | name: 'idea', 7 | component: DEFAULT_LAYOUT, 8 | meta: { 9 | locale: 'menu.workshop', 10 | requiresAuth: true, 11 | icon: 'icon-list', 12 | order: 3, 13 | }, 14 | children: [ 15 | { 16 | path: 'firmware', 17 | name: 'ideaFirmware', 18 | component: () => import('@/views/idea/firmware/index.vue'), 19 | meta: { 20 | locale: 'menu.firmware', 21 | requiresAuth: true, 22 | roles: ['*'], 23 | }, 24 | }, 25 | { 26 | path: 'Image', 27 | name: 'ideaImage', 28 | component: () => import('@/views/idea/image/index.vue'), 29 | meta: { 30 | locale: 'menu.image', 31 | requiresAuth: true, 32 | roles: ['*'], 33 | }, 34 | }, 35 | { 36 | path: 'channel', 37 | name: 'ideaChannel', 38 | component: () => import('@/views/idea/channel/index.vue'), 39 | meta: { 40 | locale: 'menu.channel', 41 | requiresAuth: true, 42 | roles: ['*'], 43 | }, 44 | }, 45 | { 46 | path: 'losehu', 47 | name: 'ideaLosehu', 48 | component: () => import('@/views/idea/losehu/index.vue'), 49 | meta: { 50 | locale: 'idea.diy', 51 | requiresAuth: true, 52 | roles: ['*'], 53 | }, 54 | }, 55 | ], 56 | }; 57 | 58 | export default IDEA; 59 | -------------------------------------------------------------------------------- /src/router/routes/modules/list.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LAYOUT } from '../base'; 2 | import { AppRouteRecordRaw } from '../types'; 3 | 4 | const LIST: AppRouteRecordRaw = { 5 | path: '/tool', 6 | name: 'list', 7 | component: DEFAULT_LAYOUT, 8 | meta: { 9 | locale: 'menu.list', 10 | requiresAuth: true, 11 | icon: 'icon-apps', 12 | order: 2, 13 | }, 14 | children: [ 15 | { 16 | path: 'backup', 17 | name: 'Backup', 18 | component: () => import('@/views/list/card/index.vue'), 19 | meta: { 20 | locale: 'menu.rb', 21 | requiresAuth: true, 22 | roles: ['*'], 23 | }, 24 | }, 25 | { 26 | path: 'flash', 27 | name: 'Flash', 28 | component: () => import('@/views/list/flash/index.vue'), 29 | meta: { 30 | locale: 'menu.flash', 31 | requiresAuth: true, 32 | roles: ['*'], 33 | }, 34 | }, 35 | { 36 | path: 'image', 37 | name: 'Image', 38 | component: () => import('@/views/list/image/index.vue'), 39 | meta: { 40 | locale: 'menu.image', 41 | requiresAuth: true, 42 | roles: ['*'], 43 | }, 44 | }, 45 | { 46 | path: 'chi', 47 | name: 'Chi', 48 | component: () => import('@/views/list/chi/index.vue'), 49 | meta: { 50 | locale: 'menu.font', 51 | requiresAuth: true, 52 | roles: ['*'], 53 | }, 54 | }, 55 | { 56 | path: 'sat', 57 | name: 'Sat', 58 | component: () => import('@/views/list/sat/index.vue'), 59 | meta: { 60 | locale: 'menu.satellite', 61 | requiresAuth: true, 62 | roles: ['*'], 63 | }, 64 | }, 65 | { 66 | path: 'sat2', 67 | name: 'Sat2', 68 | component: () => import('@/views/list/sat2/index.vue'), 69 | meta: { 70 | locale: 'menu.satellite2', 71 | requiresAuth: true, 72 | roles: ['*'], 73 | }, 74 | }, 75 | { 76 | path: 'bl', 77 | name: 'BL', 78 | component: () => import('@/views/list/bl/index.vue'), 79 | meta: { 80 | locale: 'bl', 81 | requiresAuth: true, 82 | roles: ['*'], 83 | }, 84 | }, 85 | { 86 | path: 'chat', 87 | name: 'Chat', 88 | component: () => import('@/views/list/chat/index.vue'), 89 | meta: { 90 | locale: 'chat', 91 | requiresAuth: true, 92 | roles: ['*'], 93 | }, 94 | } 95 | ], 96 | }; 97 | 98 | export default LIST; 99 | -------------------------------------------------------------------------------- /src/router/routes/types.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue'; 2 | import type { RouteMeta, NavigationGuard } from 'vue-router'; 3 | 4 | export type Component = 5 | | ReturnType 6 | | (() => Promise) 7 | | (() => Promise); 8 | 9 | export interface AppRouteRecordRaw { 10 | path: string; 11 | name?: string | symbol; 12 | meta?: RouteMeta; 13 | redirect?: string; 14 | component: Component | string; 15 | children?: AppRouteRecordRaw[]; 16 | alias?: string | string[]; 17 | props?: Record; 18 | beforeEnter?: NavigationGuard | NavigationGuard[]; 19 | fullPath?: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/router/typings.d.ts: -------------------------------------------------------------------------------- 1 | import 'vue-router'; 2 | 3 | declare module 'vue-router' { 4 | interface RouteMeta { 5 | roles?: string[]; // Controls roles that have access to the page 6 | requiresAuth: boolean; // Whether login is required to access the current page (every route must declare) 7 | icon?: string; // The icon show in the side menu 8 | locale?: string; // The locale name show in side menu and breadcrumb 9 | hideInMenu?: boolean; // If true, it is not displayed in the side menu 10 | hideChildrenInMenu?: boolean; // if set true, the children are not displayed in the side menu 11 | activeMenu?: string; // if set name, the menu will be highlighted according to the name you set 12 | order?: number; // Sort routing menu items. If set key, the higher the value, the more forward it is 13 | noAffix?: boolean; // if set true, the tag will not affix in the tab-bar 14 | ignoreCache?: boolean; // if set true, the page will not be cached 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | import useAppStore from './modules/app'; 3 | import useUserStore from './modules/user'; 4 | import useTabBarStore from './modules/tab-bar'; 5 | 6 | const pinia = createPinia(); 7 | 8 | export { useAppStore, useUserStore, useTabBarStore }; 9 | export default pinia; 10 | -------------------------------------------------------------------------------- /src/store/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { Notification } from '@arco-design/web-vue'; 3 | import type { NotificationReturn } from '@arco-design/web-vue/es/notification/interface'; 4 | import type { RouteRecordNormalized } from 'vue-router'; 5 | import defaultSettings from '@/config/settings.json'; 6 | import { getMenuList } from '@/api/user'; 7 | import { AppState } from './types'; 8 | 9 | const useAppStore = defineStore('app', { 10 | state: (): AppState => ({ ...defaultSettings }), 11 | 12 | getters: { 13 | appCurrentSetting(state: AppState): AppState { 14 | return { ...state }; 15 | }, 16 | appDevice(state: AppState) { 17 | return state.device; 18 | }, 19 | appAsyncMenus(state: AppState): RouteRecordNormalized[] { 20 | return state.serverMenu as unknown as RouteRecordNormalized[]; 21 | }, 22 | }, 23 | 24 | actions: { 25 | // Update app settings 26 | updateSettings(partial: Partial) { 27 | // @ts-ignore-next-line 28 | this.$patch(partial); 29 | }, 30 | 31 | // Change theme color 32 | toggleTheme(dark: boolean) { 33 | if (dark) { 34 | this.theme = 'dark'; 35 | document.documentElement.setAttribute('theme-mode', 'dark'); 36 | document.body.setAttribute('arco-theme', 'dark'); 37 | } else { 38 | this.theme = 'light'; 39 | document.documentElement.removeAttribute('theme-mode'); 40 | document.body.removeAttribute('arco-theme'); 41 | } 42 | }, 43 | toggleDevice(device: string) { 44 | this.device = device; 45 | }, 46 | toggleMenu(value: boolean) { 47 | this.hideMenu = value; 48 | }, 49 | async fetchServerMenuConfig() { 50 | let notifyInstance: NotificationReturn | null = null; 51 | try { 52 | notifyInstance = Notification.info({ 53 | id: 'menuNotice', // Keep the instance id the same 54 | content: 'loading', 55 | closable: true, 56 | }); 57 | const { data } = await getMenuList(); 58 | this.serverMenu = data; 59 | notifyInstance = Notification.success({ 60 | id: 'menuNotice', 61 | content: 'success', 62 | closable: true, 63 | }); 64 | } catch (error) { 65 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 66 | notifyInstance = Notification.error({ 67 | id: 'menuNotice', 68 | content: 'error', 69 | closable: true, 70 | }); 71 | } 72 | }, 73 | clearServerMenu() { 74 | this.serverMenu = []; 75 | }, 76 | }, 77 | }); 78 | 79 | export default useAppStore; 80 | -------------------------------------------------------------------------------- /src/store/modules/app/types.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordNormalized } from 'vue-router'; 2 | 3 | export interface AppState { 4 | theme: string; 5 | colorWeak: boolean; 6 | navbar: boolean; 7 | menu: boolean; 8 | topMenu: boolean; 9 | hideMenu: boolean; 10 | menuCollapse: boolean; 11 | footer: boolean; 12 | themeColor: string; 13 | menuWidth: number; 14 | globalSettings: boolean; 15 | device: string; 16 | tabBar: boolean; 17 | menuFromServer: boolean; 18 | serverMenu: RouteRecordNormalized[]; 19 | [key: string]: unknown; 20 | } 21 | -------------------------------------------------------------------------------- /src/store/modules/tab-bar/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalized } from 'vue-router'; 2 | import { defineStore } from 'pinia'; 3 | import { 4 | DEFAULT_ROUTE, 5 | DEFAULT_ROUTE_NAME, 6 | REDIRECT_ROUTE_NAME, 7 | } from '@/router/constants'; 8 | import { isString } from '@/utils/is'; 9 | import { TabBarState, TagProps } from './types'; 10 | 11 | const formatTag = (route: RouteLocationNormalized): TagProps => { 12 | const { name, meta, fullPath, query } = route; 13 | return { 14 | title: meta.locale || '', 15 | name: String(name), 16 | fullPath, 17 | query, 18 | ignoreCache: meta.ignoreCache, 19 | }; 20 | }; 21 | 22 | const BAN_LIST = [REDIRECT_ROUTE_NAME]; 23 | 24 | const useAppStore = defineStore('tabBar', { 25 | state: (): TabBarState => ({ 26 | cacheTabList: new Set([DEFAULT_ROUTE_NAME]), 27 | tagList: [DEFAULT_ROUTE], 28 | }), 29 | 30 | getters: { 31 | getTabList(): TagProps[] { 32 | return this.tagList; 33 | }, 34 | getCacheList(): string[] { 35 | return Array.from(this.cacheTabList); 36 | }, 37 | }, 38 | 39 | actions: { 40 | updateTabList(route: RouteLocationNormalized) { 41 | if (BAN_LIST.includes(route.name as string)) return; 42 | this.tagList.push(formatTag(route)); 43 | if (!route.meta.ignoreCache) { 44 | this.cacheTabList.add(route.name as string); 45 | } 46 | }, 47 | deleteTag(idx: number, tag: TagProps) { 48 | this.tagList.splice(idx, 1); 49 | this.cacheTabList.delete(tag.name); 50 | }, 51 | addCache(name: string) { 52 | if (isString(name) && name !== '') this.cacheTabList.add(name); 53 | }, 54 | deleteCache(tag: TagProps) { 55 | this.cacheTabList.delete(tag.name); 56 | }, 57 | freshTabList(tags: TagProps[]) { 58 | this.tagList = tags; 59 | this.cacheTabList.clear(); 60 | // 要先判断ignoreCache 61 | this.tagList 62 | .filter((el) => !el.ignoreCache) 63 | .map((el) => el.name) 64 | .forEach((x) => this.cacheTabList.add(x)); 65 | }, 66 | resetTabList() { 67 | this.tagList = [DEFAULT_ROUTE]; 68 | this.cacheTabList.clear(); 69 | this.cacheTabList.add(DEFAULT_ROUTE_NAME); 70 | }, 71 | }, 72 | }); 73 | 74 | export default useAppStore; 75 | -------------------------------------------------------------------------------- /src/store/modules/tab-bar/types.ts: -------------------------------------------------------------------------------- 1 | export interface TagProps { 2 | title: string; 3 | name: string; 4 | fullPath: string; 5 | query?: any; 6 | ignoreCache?: boolean; 7 | } 8 | 9 | export interface TabBarState { 10 | tagList: TagProps[]; 11 | cacheTabList: Set; 12 | } 13 | -------------------------------------------------------------------------------- /src/store/modules/user/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { 3 | login as userLogin, 4 | logout as userLogout, 5 | getUserInfo, 6 | LoginData, 7 | } from '@/api/user'; 8 | import { setToken, clearToken } from '@/utils/auth'; 9 | import { removeRouteListener } from '@/utils/route-listener'; 10 | import { UserState } from './types'; 11 | import useAppStore from '../app'; 12 | 13 | const useUserStore = defineStore('user', { 14 | state: (): UserState => ({ 15 | name: undefined, 16 | avatar: undefined, 17 | job: undefined, 18 | organization: undefined, 19 | location: undefined, 20 | email: undefined, 21 | introduction: undefined, 22 | personalWebsite: undefined, 23 | jobName: undefined, 24 | organizationName: undefined, 25 | locationName: undefined, 26 | phone: undefined, 27 | registrationDate: undefined, 28 | accountId: undefined, 29 | certification: undefined, 30 | role: '', 31 | showLogin: false, 32 | showRegister: false 33 | }), 34 | 35 | getters: { 36 | userInfo(state: UserState): UserState { 37 | return { ...state }; 38 | }, 39 | }, 40 | 41 | actions: { 42 | switchRoles() { 43 | return new Promise((resolve) => { 44 | this.role = this.role === 'user' ? 'admin' : 'user'; 45 | resolve(this.role); 46 | }); 47 | }, 48 | // Set user's information 49 | setInfo(partial: Partial) { 50 | this.$patch(partial); 51 | }, 52 | 53 | // Reset user's information 54 | resetInfo() { 55 | this.$reset(); 56 | }, 57 | 58 | // Get user's information 59 | async info() { 60 | const res = await getUserInfo(); 61 | 62 | this.setInfo(res.data); 63 | }, 64 | 65 | // Login 66 | async login(loginForm: LoginData) { 67 | try { 68 | const res = await userLogin(loginForm); 69 | setToken(res.data.token); 70 | } catch (err) { 71 | clearToken(); 72 | throw err; 73 | } 74 | }, 75 | logoutCallBack() { 76 | const appStore = useAppStore(); 77 | this.resetInfo(); 78 | clearToken(); 79 | removeRouteListener(); 80 | appStore.clearServerMenu(); 81 | }, 82 | // Logout 83 | async logout() { 84 | // try { 85 | // await userLogout(); 86 | // } finally { 87 | // this.logoutCallBack(); 88 | // } 89 | this.logoutCallBack(); 90 | }, 91 | }, 92 | }); 93 | 94 | export default useUserStore; 95 | -------------------------------------------------------------------------------- /src/store/modules/user/types.ts: -------------------------------------------------------------------------------- 1 | export type RoleType = '' | '*' | 'admin' | 'user'; 2 | export interface UserState { 3 | name?: string; 4 | avatar?: string; 5 | job?: string; 6 | organization?: string; 7 | location?: string; 8 | email?: string; 9 | introduction?: string; 10 | personalWebsite?: string; 11 | jobName?: string; 12 | organizationName?: string; 13 | locationName?: string; 14 | phone?: string; 15 | registrationDate?: string; 16 | accountId?: string; 17 | certification?: number; 18 | role: RoleType; 19 | showLogin: boolean; 20 | showRegister: boolean; 21 | } 22 | -------------------------------------------------------------------------------- /src/types/echarts.ts: -------------------------------------------------------------------------------- 1 | import { CallbackDataParams } from 'echarts/types/dist/shared'; 2 | 3 | export interface ToolTipFormatterParams extends CallbackDataParams { 4 | axisDim: string; 5 | axisIndex: number; 6 | axisType: string; 7 | axisId: string; 8 | axisValue: string; 9 | axisValueLabel: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/global.ts: -------------------------------------------------------------------------------- 1 | export interface AnyObject { 2 | [key: string]: unknown; 3 | } 4 | 5 | export interface Options { 6 | value: unknown; 7 | label: string; 8 | } 9 | 10 | export interface NodeOptions extends Options { 11 | children?: NodeOptions[]; 12 | } 13 | 14 | export interface GetParams { 15 | body: null; 16 | type: string; 17 | url: string; 18 | } 19 | 20 | export interface PostData { 21 | body: string; 22 | type: string; 23 | url: string; 24 | } 25 | 26 | export interface Pagination { 27 | current: number; 28 | pageSize: number; 29 | total?: number; 30 | } 31 | 32 | export type TimeRanger = [string, string]; 33 | 34 | export interface GeneralChart { 35 | xAxis: string[]; 36 | data: Array<{ name: string; value: number[] }>; 37 | } 38 | -------------------------------------------------------------------------------- /src/types/mock.ts: -------------------------------------------------------------------------------- 1 | export interface MockParams { 2 | url: string; 3 | type: string; 4 | body: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/AutoUpdate.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 前端重新部署通知用户刷新网页 4 | */ 5 | 6 | class Updater { 7 | oldScript = []; // 存储第一次值也就是script 的hash 信息 8 | newScript = []; // 获取新的值 也就是新的script 的hash信息 9 | dispatch = {}; // 小型发布订阅通知用户更新了 10 | 11 | constructor() { 12 | this.oldScript = []; 13 | this.newScript = []; 14 | this.dispatch = {}; 15 | this.init(); // 初始化 16 | this.timing(); 17 | } 18 | 19 | async init() { 20 | const html = await this.getHtml(); 21 | this.oldScript = this.parserScript(html); 22 | }; 23 | 24 | async getHtml() { 25 | const html = await fetch('/').then(res => res.text());//读取index html 26 | return html 27 | }; 28 | 29 | parserScript(html) { 30 | const reg = new RegExp(/]*)?>(.*?)<\/script\s*>/ig) //script正则 31 | return html.match(reg) //匹配script标签 32 | } 33 | 34 | //发布订阅通知 35 | on(key, fn) { 36 | (this.dispatch[key] || (this.dispatch[key] = [])).push(fn) 37 | return this; 38 | } 39 | 40 | compare(oldArr, newArr) { 41 | const base = oldArr.length; 42 | const arr = Array.from(new Set(oldArr.concat(newArr))); 43 | //如果新旧length 一样无更新 44 | if (arr.length === base) { 45 | // this.dispatch['no-update'].forEach(fn => { 46 | // fn(); 47 | // }) 48 | } else { 49 | // 否则通知更新 50 | this.dispatch['update'].forEach(fn => { 51 | fn(); 52 | }) 53 | } 54 | }; 55 | 56 | async timing() { 57 | setInterval(async () => { 58 | const newHtml = await this.getHtml(); 59 | this.newScript = this.parserScript(newHtml); 60 | this.compare(this.oldScript, this.newScript) 61 | //这边给的是默认值15000,也可以自定义秒数 62 | }, 60000); 63 | }; 64 | } 65 | 66 | export default Updater; -------------------------------------------------------------------------------- /src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | const TOKEN_KEY = 'token'; 2 | 3 | const isLogin = () => { 4 | return !!localStorage.getItem(TOKEN_KEY); 5 | }; 6 | 7 | const getToken = () => { 8 | return localStorage.getItem(TOKEN_KEY); 9 | }; 10 | 11 | const setToken = (token: string) => { 12 | localStorage.setItem(TOKEN_KEY, token); 13 | }; 14 | 15 | const clearToken = () => { 16 | localStorage.removeItem(TOKEN_KEY); 17 | }; 18 | 19 | export { isLogin, getToken, setToken, clearToken }; 20 | -------------------------------------------------------------------------------- /src/utils/env.ts: -------------------------------------------------------------------------------- 1 | const debug = import.meta.env.MODE !== 'production'; 2 | 3 | export default debug; 4 | -------------------------------------------------------------------------------- /src/utils/event.ts: -------------------------------------------------------------------------------- 1 | export function addEventListen( 2 | target: Window | HTMLElement, 3 | event: string, 4 | handler: EventListenerOrEventListenerObject, 5 | capture = false 6 | ) { 7 | if ( 8 | target.addEventListener && 9 | typeof target.addEventListener === 'function' 10 | ) { 11 | target.addEventListener(event, handler, capture); 12 | } 13 | } 14 | 15 | export function removeEventListen( 16 | target: Window | HTMLElement, 17 | event: string, 18 | handler: EventListenerOrEventListenerObject, 19 | capture = false 20 | ) { 21 | if ( 22 | target.removeEventListener && 23 | typeof target.removeEventListener === 'function' 24 | ) { 25 | target.removeEventListener(event, handler, capture); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | type TargetContext = '_self' | '_parent' | '_blank' | '_top'; 2 | 3 | export const openWindow = ( 4 | url: string, 5 | opts?: { target?: TargetContext; [key: string]: any } 6 | ) => { 7 | const { target = '_blank', ...others } = opts || {}; 8 | window.open( 9 | url, 10 | target, 11 | Object.entries(others) 12 | .reduce((preValue: string[], curValue) => { 13 | const [key, value] = curValue; 14 | return [...preValue, `${key}=${value}`]; 15 | }, []) 16 | .join(',') 17 | ); 18 | }; 19 | 20 | export const regexUrl = new RegExp( 21 | '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$', 22 | 'i' 23 | ); 24 | 25 | export default null; 26 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | const opt = Object.prototype.toString; 2 | 3 | export function isArray(obj: any): obj is any[] { 4 | return opt.call(obj) === '[object Array]'; 5 | } 6 | 7 | export function isObject(obj: any): obj is { [key: string]: any } { 8 | return opt.call(obj) === '[object Object]'; 9 | } 10 | 11 | export function isString(obj: any): obj is string { 12 | return opt.call(obj) === '[object String]'; 13 | } 14 | 15 | export function isNumber(obj: any): obj is number { 16 | return opt.call(obj) === '[object Number]' && obj === obj; // eslint-disable-line 17 | } 18 | 19 | export function isRegExp(obj: any) { 20 | return opt.call(obj) === '[object RegExp]'; 21 | } 22 | 23 | export function isFile(obj: any): obj is File { 24 | return opt.call(obj) === '[object File]'; 25 | } 26 | 27 | export function isBlob(obj: any): obj is Blob { 28 | return opt.call(obj) === '[object Blob]'; 29 | } 30 | 31 | export function isUndefined(obj: any): obj is undefined { 32 | return obj === undefined; 33 | } 34 | 35 | export function isNull(obj: any): obj is null { 36 | return obj === null; 37 | } 38 | 39 | export function isFunction(obj: any): obj is (...args: any[]) => any { 40 | return typeof obj === 'function'; 41 | } 42 | 43 | export function isEmptyObject(obj: any): boolean { 44 | return isObject(obj) && Object.keys(obj).length === 0; 45 | } 46 | 47 | export function isExist(obj: any): boolean { 48 | return obj || obj === 0; 49 | } 50 | 51 | export function isWindow(el: any): el is Window { 52 | return el === window; 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/monitor.ts: -------------------------------------------------------------------------------- 1 | import { App, ComponentPublicInstance } from 'vue'; 2 | import axios from 'axios'; 3 | 4 | export default function handleError(Vue: App, baseUrl: string) { 5 | if (!baseUrl) { 6 | return; 7 | } 8 | Vue.config.errorHandler = ( 9 | err: unknown, 10 | instance: ComponentPublicInstance | null, 11 | info: string 12 | ) => { 13 | // send error info 14 | axios.post(`${baseUrl}/report-error`, { 15 | err, 16 | instance, 17 | info, 18 | // location: window.location.href, 19 | // message: err.message, 20 | // stack: err.stack, 21 | // browserInfo: getBrowserInfo(), 22 | // user info 23 | // dom info 24 | // url info 25 | // ... 26 | }); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/route-listener.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Listening to routes alone would waste rendering performance. Use the publish-subscribe model for distribution management 3 | * 单独监听路由会浪费渲染性能。使用发布订阅模式去进行分发管理。 4 | */ 5 | import mitt, { Handler } from 'mitt'; 6 | import type { RouteLocationNormalized } from 'vue-router'; 7 | 8 | const emitter = mitt(); 9 | 10 | const key = Symbol('ROUTE_CHANGE'); 11 | 12 | let latestRoute: RouteLocationNormalized; 13 | 14 | export function setRouteEmitter(to: RouteLocationNormalized) { 15 | emitter.emit(key, to); 16 | latestRoute = to; 17 | } 18 | 19 | export function listenerRouteChange( 20 | handler: (route: RouteLocationNormalized) => void, 21 | immediate = true 22 | ) { 23 | emitter.on(key, handler as Handler); 24 | if (immediate && latestRoute) { 25 | handler(latestRoute); 26 | } 27 | } 28 | 29 | export function removeRouteListener() { 30 | emitter.off(key); 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/setup-mock.ts: -------------------------------------------------------------------------------- 1 | import debug from './env'; 2 | 3 | export default ({ mock, setup }: { mock?: boolean; setup: () => void }) => { 4 | if (mock !== false && debug) setup(); 5 | }; 6 | 7 | export const successResponseWrap = (data: unknown) => { 8 | return { 9 | data, 10 | status: 'ok', 11 | msg: '请求成功', 12 | code: 20000, 13 | }; 14 | }; 15 | 16 | export const failResponseWrap = (data: unknown, msg: string, code = 50000) => { 17 | return { 18 | data, 19 | status: 'fail', 20 | msg, 21 | code, 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/views/dashboard/workplace/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 20 | 21 | 92 | 93 | 107 | -------------------------------------------------------------------------------- /src/views/dashboard/workplace/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.dashboard.workplace': 'Basic Information', 3 | 'workplace.welcome': 'Welcome~, click the "Connect" button in the upper right corner to connect the UV-K5.', 4 | 'workplace.welcomeSuc': 'Welcome~, connection successful.', 5 | 'workplace.info': 'Information', 6 | 'workplace.current': 'Current Firmware Version: ', 7 | 'workplace.writeconfig': 'Write Configuration: ', 8 | 'workplace.eepromSize': 'EEPROM Size: ', 9 | 'workplace.clickCheck': 'Click the TEST button to test', 10 | 'workplace.checkIt': 'TEST', 11 | 'workplace.unk': 'Unknown / Faulty / Unavailable', 12 | 'workplace.balance': 'Balance (CNY)', 13 | 'workplace.order.pending': 'Pending', 14 | 'workplace.order.pendingRenewal': 'Renewal Order', 15 | 'workplace.onlineContent': 'Online Content', 16 | 'workplace.putIn': 'Put In', 17 | 'workplace.newDay': 'Daily Additional Comments', 18 | 'workplace.newFromYesterday': 'New From Yesterday', 19 | 'workplace.minute': 'Min', 20 | 'workplace.docs': 'Documents', 21 | 'workplace.docs.productOverview': 'Product Overview', 22 | 'workplace.docs.userGuide': 'User Guide', 23 | 'workplace.docs.workflow': 'Workflow', 24 | 'workplace.docs.interfaceDocs': 'Interface Docs', 25 | // 26 | 'workplace.contentManagement': 'Content Management', 27 | 'workplace.contentStatistical': 'Content Statistical', 28 | 'workplace.advanced': 'Advanced', 29 | 'workplace.onlinePromotion': 'Online Promotion', 30 | 'workplace.contentPutIn': 'Put In', 31 | 'workplace.announcement': 'Announcement', 32 | 'workplace.recently.visited': 'Recently Visited', 33 | 'workplace.record.nodata': 'No data', 34 | 'workplace.quick.operation': 'Quick Operation', 35 | 'workplace.quickOperation.setup': 'Setup', 36 | 'workplace.allProject': 'All', 37 | 'workplace.loadMore': 'More', 38 | 'workplace.viewMore': 'More', 39 | 'workplace.contentData': 'Content Data', 40 | 'workplace.popularContent': 'Popular Content', 41 | 'workplace.popularContent.text': 'text', 42 | 'workplace.popularContent.image': 'image', 43 | 'workplace.popularContent.video': 'video', 44 | 'workplace.categoriesPercent': 'Categories Percent', 45 | 'workplace.pecs': 'pecs', 46 | }; 47 | -------------------------------------------------------------------------------- /src/views/dashboard/workplace/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.dashboard.workplace': '基础信息', 3 | 'workplace.welcome': '欢迎你~,点击右上角“连接”按钮连接手台。', 4 | 'workplace.welcomeSuc': '欢迎你~,连接成功!', 5 | 'workplace.info': '手台信息', 6 | 'workplace.current': '当前固件版本:', 7 | 'workplace.writeconfig': '匹配写频配置:', 8 | 'workplace.eepromSize': '存储大小:', 9 | 'workplace.clickCheck': '点击检测按钮检测', 10 | 'workplace.checkIt': '检测', 11 | 'workplace.unk': '未知、故障、不可用', 12 | 'workplace.balance': '余额(元)', 13 | 'workplace.order.pending': '待支付', 14 | 'workplace.order.pendingRenewal': '待续费订单', 15 | 'workplace.onlineContent': '线上总内容', 16 | 'workplace.putIn': '投放中内容', 17 | 'workplace.newDay': '日新增评论', 18 | 'workplace.newFromYesterday': '较昨日新增', 19 | 'workplace.minute': '分钟', 20 | 'workplace.docs': '帮助文档', 21 | 'workplace.docs.productOverview': '产品概要', 22 | 'workplace.docs.userGuide': '使用指南', 23 | 'workplace.docs.workflow': '接入流程', 24 | 'workplace.docs.interfaceDocs': '接口文档', 25 | 'workplace.contentManagement': '内容管理', 26 | 'workplace.contentStatistical': '内容分析', 27 | 'workplace.advanced': '高级管理', 28 | 'workplace.onlinePromotion': '线上推广', 29 | 'workplace.contentPutIn': '内容投放', 30 | 'workplace.announcement': '公告', 31 | 'workplace.recently.visited': '最近访问', 32 | 'workplace.record.nodata': '暂无数据', 33 | 'workplace.quick.operation': '快捷操作', 34 | 'workplace.quickOperation.setup': '管理', 35 | 'workplace.allProject': '所有项目', 36 | 'workplace.loadMore': '加载更多', 37 | 'workplace.viewMore': '查看更多', 38 | 'workplace.contentData': '内容数据', 39 | 'workplace.popularContent': '线上热门内容', 40 | 'workplace.popularContent.text': '文本', 41 | 'workplace.popularContent.image': '图片', 42 | 'workplace.popularContent.video': '视频', 43 | 'workplace.categoriesPercent': '内容类型占比', 44 | 'workplace.pecs': '个', 45 | }; 46 | -------------------------------------------------------------------------------- /src/views/guide/f117/assets/cj1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/src/views/guide/f117/assets/cj1.png -------------------------------------------------------------------------------- /src/views/guide/f117/assets/cj2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/src/views/guide/f117/assets/cj2.png -------------------------------------------------------------------------------- /src/views/guide/f117/assets/cj3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenty4ng/k5web/1bfc9a05ce3975d8fb24c6dee9aae6b0afc2b474/src/views/guide/f117/assets/cj3.png -------------------------------------------------------------------------------- /src/views/guide/f117/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.cardList': 'Card List', 3 | 'cardList.tab.title.all': 'All', 4 | 'cardList.tab.title.content': 'Quality Inspection', 5 | 'cardList.tab.title.service': 'The service', 6 | 'cardList.tab.title.preset': 'Rules Preset', 7 | 'cardList.searchInput.placeholder': 'Search', 8 | 'cardList.enable': 'Enable', 9 | 'cardList.disable': 'Disable', 10 | 'cardList.content.delete': 'Delete', 11 | 'cardList.content.inspection': 'Inspection', 12 | 'cardList.content.action': 'Click Create Qc Content queue', 13 | 'cardList.service.open': 'Open', 14 | 'cardList.service.cancel': 'Cancel', 15 | 'cardList.service.renew': 'Contract of service', 16 | 'cardList.service.tag': 'Opened', 17 | 'cardList.service.expiresTag': 'Expired', 18 | 'cardList.preset.tag': 'Enable', 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/guide/f117/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.cardList': '卡片列表', 3 | 'cardList.tab.title.all': '全部', 4 | 'cardList.tab.title.content': '内容质检', 5 | 'cardList.tab.title.service': '开通服务', 6 | 'cardList.tab.title.preset': '规则预置', 7 | 'cardList.searchInput.placeholder': '搜索', 8 | // 'cardList.statistic.enable': '已启用', 9 | // 'cardList.statistic.disable': '未启用', 10 | 'cardList.content.delete': '删除', 11 | 'cardList.content.inspection': '质检', 12 | 'cardList.content.action': '点击创建质检内容队列', 13 | 'cardList.service.open': '开通服务', 14 | 'cardList.service.cancel': '取消服务', 15 | 'cardList.service.renew': '续约服务', 16 | 'cardList.service.tag': '已开通', 17 | 'cardList.service.expiresTag': '已过期', 18 | 'cardList.preset.tag': '已启用', 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/idea/losehu/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 144 | 145 | 150 | 151 | -------------------------------------------------------------------------------- /src/views/list/card/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.cardList': 'Card List', 3 | 'cardList.tab.title.all': 'All', 4 | 'cardList.tab.title.content': 'Quality Inspection', 5 | 'cardList.tab.title.service': 'The service', 6 | 'cardList.tab.title.preset': 'Rules Preset', 7 | 'cardList.searchInput.placeholder': 'Search', 8 | 'cardList.enable': 'Enable', 9 | 'cardList.disable': 'Disable', 10 | 'cardList.content.delete': 'Delete', 11 | 'cardList.content.inspection': 'Inspection', 12 | 'cardList.content.action': 'Click Create Qc Content queue', 13 | 'cardList.service.open': 'Open', 14 | 'cardList.service.cancel': 'Cancel', 15 | 'cardList.service.renew': 'Contract of service', 16 | 'cardList.service.tag': 'Opened', 17 | 'cardList.service.expiresTag': 'Expired', 18 | 'cardList.preset.tag': 'Enable', 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/list/card/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.cardList': '卡片列表', 3 | 'cardList.tab.title.all': '全部', 4 | 'cardList.tab.title.content': '内容质检', 5 | 'cardList.tab.title.service': '开通服务', 6 | 'cardList.tab.title.preset': '规则预置', 7 | 'cardList.searchInput.placeholder': '搜索', 8 | // 'cardList.statistic.enable': '已启用', 9 | // 'cardList.statistic.disable': '未启用', 10 | 'cardList.content.delete': '删除', 11 | 'cardList.content.inspection': '质检', 12 | 'cardList.content.action': '点击创建质检内容队列', 13 | 'cardList.service.open': '开通服务', 14 | 'cardList.service.cancel': '取消服务', 15 | 'cardList.service.renew': '续约服务', 16 | 'cardList.service.tag': '已开通', 17 | 'cardList.service.expiresTag': '已过期', 18 | 'cardList.preset.tag': '已启用', 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/list/chat/index.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 193 | 194 | 199 | 200 | -------------------------------------------------------------------------------- /src/views/list/chi/index.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 179 | 180 | 185 | 186 | 230 | -------------------------------------------------------------------------------- /src/views/list/chi/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.cardList': 'Card List', 3 | 'cardList.tab.title.all': 'All', 4 | 'cardList.tab.title.content': 'Quality Inspection', 5 | 'cardList.tab.title.service': 'The service', 6 | 'cardList.tab.title.preset': 'Rules Preset', 7 | 'cardList.searchInput.placeholder': 'Search', 8 | 'cardList.enable': 'Enable', 9 | 'cardList.disable': 'Disable', 10 | 'cardList.content.delete': 'Delete', 11 | 'cardList.content.inspection': 'Inspection', 12 | 'cardList.content.action': 'Click Create Qc Content queue', 13 | 'cardList.service.open': 'Open', 14 | 'cardList.service.cancel': 'Cancel', 15 | 'cardList.service.renew': 'Contract of service', 16 | 'cardList.service.tag': 'Opened', 17 | 'cardList.service.expiresTag': 'Expired', 18 | 'cardList.preset.tag': 'Enable', 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/list/chi/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.cardList': '卡片列表', 3 | 'cardList.tab.title.all': '全部', 4 | 'cardList.tab.title.content': '内容质检', 5 | 'cardList.tab.title.service': '开通服务', 6 | 'cardList.tab.title.preset': '规则预置', 7 | 'cardList.searchInput.placeholder': '搜索', 8 | // 'cardList.statistic.enable': '已启用', 9 | // 'cardList.statistic.disable': '未启用', 10 | 'cardList.content.delete': '删除', 11 | 'cardList.content.inspection': '质检', 12 | 'cardList.content.action': '点击创建质检内容队列', 13 | 'cardList.service.open': '开通服务', 14 | 'cardList.service.cancel': '取消服务', 15 | 'cardList.service.renew': '续约服务', 16 | 'cardList.service.tag': '已开通', 17 | 'cardList.service.expiresTag': '已过期', 18 | 'cardList.preset.tag': '已启用', 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/list/flash/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 143 | 144 | 149 | 150 | 200 | -------------------------------------------------------------------------------- /src/views/list/flash/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.cardList': 'Card List', 3 | 'cardList.tab.title.all': 'All', 4 | 'cardList.tab.title.content': 'Quality Inspection', 5 | 'cardList.tab.title.service': 'The service', 6 | 'cardList.tab.title.preset': 'Rules Preset', 7 | 'cardList.searchInput.placeholder': 'Search', 8 | 'cardList.enable': 'Enable', 9 | 'cardList.disable': 'Disable', 10 | 'cardList.content.delete': 'Delete', 11 | 'cardList.content.inspection': 'Inspection', 12 | 'cardList.content.action': 'Click Create Qc Content queue', 13 | 'cardList.service.open': 'Open', 14 | 'cardList.service.cancel': 'Cancel', 15 | 'cardList.service.renew': 'Contract of service', 16 | 'cardList.service.tag': 'Opened', 17 | 'cardList.service.expiresTag': 'Expired', 18 | 'cardList.preset.tag': 'Enable', 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/list/flash/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.cardList': '卡片列表', 3 | 'cardList.tab.title.all': '全部', 4 | 'cardList.tab.title.content': '内容质检', 5 | 'cardList.tab.title.service': '开通服务', 6 | 'cardList.tab.title.preset': '规则预置', 7 | 'cardList.searchInput.placeholder': '搜索', 8 | // 'cardList.statistic.enable': '已启用', 9 | // 'cardList.statistic.disable': '未启用', 10 | 'cardList.content.delete': '删除', 11 | 'cardList.content.inspection': '质检', 12 | 'cardList.content.action': '点击创建质检内容队列', 13 | 'cardList.service.open': '开通服务', 14 | 'cardList.service.cancel': '取消服务', 15 | 'cardList.service.renew': '续约服务', 16 | 'cardList.service.tag': '已开通', 17 | 'cardList.service.expiresTag': '已过期', 18 | 'cardList.preset.tag': '已启用', 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/list/image/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.cardList': 'Card List', 3 | 'cardList.tab.title.all': 'All', 4 | 'cardList.tab.title.content': 'Quality Inspection', 5 | 'cardList.tab.title.service': 'The service', 6 | 'cardList.tab.title.preset': 'Rules Preset', 7 | 'cardList.searchInput.placeholder': 'Search', 8 | 'cardList.enable': 'Enable', 9 | 'cardList.disable': 'Disable', 10 | 'cardList.content.delete': 'Delete', 11 | 'cardList.content.inspection': 'Inspection', 12 | 'cardList.content.action': 'Click Create Qc Content queue', 13 | 'cardList.service.open': 'Open', 14 | 'cardList.service.cancel': 'Cancel', 15 | 'cardList.service.renew': 'Contract of service', 16 | 'cardList.service.tag': 'Opened', 17 | 'cardList.service.expiresTag': 'Expired', 18 | 'cardList.preset.tag': 'Enable', 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/list/image/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.cardList': '卡片列表', 3 | 'cardList.tab.title.all': '全部', 4 | 'cardList.tab.title.content': '内容质检', 5 | 'cardList.tab.title.service': '开通服务', 6 | 'cardList.tab.title.preset': '规则预置', 7 | 'cardList.searchInput.placeholder': '搜索', 8 | // 'cardList.statistic.enable': '已启用', 9 | // 'cardList.statistic.disable': '未启用', 10 | 'cardList.content.delete': '删除', 11 | 'cardList.content.inspection': '质检', 12 | 'cardList.content.action': '点击创建质检内容队列', 13 | 'cardList.service.open': '开通服务', 14 | 'cardList.service.cancel': '取消服务', 15 | 'cardList.service.renew': '续约服务', 16 | 'cardList.service.tag': '已开通', 17 | 'cardList.service.expiresTag': '已过期', 18 | 'cardList.preset.tag': '已启用', 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/list/sat/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.cardList': 'Card List', 3 | 'cardList.tab.title.all': 'All', 4 | 'cardList.tab.title.content': 'Quality Inspection', 5 | 'cardList.tab.title.service': 'The service', 6 | 'cardList.tab.title.preset': 'Rules Preset', 7 | 'cardList.searchInput.placeholder': 'Search', 8 | 'cardList.enable': 'Enable', 9 | 'cardList.disable': 'Disable', 10 | 'cardList.content.delete': 'Delete', 11 | 'cardList.content.inspection': 'Inspection', 12 | 'cardList.content.action': 'Click Create Qc Content queue', 13 | 'cardList.service.open': 'Open', 14 | 'cardList.service.cancel': 'Cancel', 15 | 'cardList.service.renew': 'Contract of service', 16 | 'cardList.service.tag': 'Opened', 17 | 'cardList.service.expiresTag': 'Expired', 18 | 'cardList.preset.tag': 'Enable', 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/list/sat/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.cardList': '卡片列表', 3 | 'cardList.tab.title.all': '全部', 4 | 'cardList.tab.title.content': '内容质检', 5 | 'cardList.tab.title.service': '开通服务', 6 | 'cardList.tab.title.preset': '规则预置', 7 | 'cardList.searchInput.placeholder': '搜索', 8 | // 'cardList.statistic.enable': '已启用', 9 | // 'cardList.statistic.disable': '未启用', 10 | 'cardList.content.delete': '删除', 11 | 'cardList.content.inspection': '质检', 12 | 'cardList.content.action': '点击创建质检内容队列', 13 | 'cardList.service.open': '开通服务', 14 | 'cardList.service.cancel': '取消服务', 15 | 'cardList.service.renew': '续约服务', 16 | 'cardList.service.tag': '已开通', 17 | 'cardList.service.expiresTag': '已过期', 18 | 'cardList.preset.tag': '已启用', 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/list/satloc/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 76 | 77 | 82 | 83 | -------------------------------------------------------------------------------- /src/views/list/search-table/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.searchTable': 'Search Table', 3 | 'searchTable.form.number': 'Set Number', 4 | 'searchTable.form.number.placeholder': 'Please enter Set Number', 5 | 'searchTable.form.name': 'Set Name', 6 | 'searchTable.form.name.placeholder': 'Please enter Set Name', 7 | 'searchTable.form.contentType': 'Content Type', 8 | 'searchTable.form.contentType.img': 'image-text', 9 | 'searchTable.form.contentType.horizontalVideo': 'Horizontal short video', 10 | 'searchTable.form.contentType.verticalVideo': 'Vertical short video', 11 | 'searchTable.form.filterType': 'Filter Type', 12 | 'searchTable.form.filterType.artificial': 'artificial', 13 | 'searchTable.form.filterType.rules': 'Rules', 14 | 'searchTable.form.createdTime': 'Create Date', 15 | 'searchTable.form.status': 'Status', 16 | 'searchTable.form.status.online': 'Online', 17 | 'searchTable.form.status.offline': 'Offline', 18 | 'searchTable.form.search': 'Search', 19 | 'searchTable.form.reset': 'Reset', 20 | 'searchTable.form.selectDefault': 'All', 21 | 'searchTable.operation.create': 'Create', 22 | 'searchTable.operation.import': 'Import', 23 | 'searchTable.operation.download': 'Download', 24 | // columns 25 | 'searchTable.columns.index': '#', 26 | 'searchTable.columns.number': 'Set Number', 27 | 'searchTable.columns.name': 'Set Name', 28 | 'searchTable.columns.contentType': 'Content Type', 29 | 'searchTable.columns.filterType': 'Filter Type', 30 | 'searchTable.columns.count': 'Count', 31 | 'searchTable.columns.createdTime': 'CreatedTime', 32 | 'searchTable.columns.status': 'Status', 33 | 'searchTable.columns.operations': 'Operations', 34 | 'searchTable.columns.operations.view': 'View', 35 | // size 36 | 'searchTable.size.mini': 'mini', 37 | 'searchTable.size.small': 'small', 38 | 'searchTable.size.medium': 'middle', 39 | 'searchTable.size.large': 'large', 40 | // actions 41 | 'searchTable.actions.refresh': 'refresh', 42 | 'searchTable.actions.density': 'density', 43 | 'searchTable.actions.columnSetting': 'columnSetting', 44 | }; 45 | -------------------------------------------------------------------------------- /src/views/list/search-table/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.searchTable': '查询表格', 3 | 'searchTable.form.number': '集合编号', 4 | 'searchTable.form.number.placeholder': '请输入集合编号', 5 | 'searchTable.form.name': '集合名称', 6 | 'searchTable.form.name.placeholder': '请输入集合名称', 7 | 'searchTable.form.contentType': '内容体裁', 8 | 'searchTable.form.contentType.img': '图文', 9 | 'searchTable.form.contentType.horizontalVideo': '横版短视频', 10 | 'searchTable.form.contentType.verticalVideo': '竖版小视频', 11 | 'searchTable.form.filterType': '筛选方式', 12 | 'searchTable.form.filterType.artificial': '人工筛选', 13 | 'searchTable.form.filterType.rules': '规则筛选', 14 | 'searchTable.form.createdTime': '创建时间', 15 | 'searchTable.form.status': '状态', 16 | 'searchTable.form.status.online': '已上线', 17 | 'searchTable.form.status.offline': '已下线', 18 | 'searchTable.form.search': '查询', 19 | 'searchTable.form.reset': '重置', 20 | 'searchTable.form.selectDefault': '全部', 21 | 'searchTable.operation.create': '新建', 22 | 'searchTable.operation.import': '批量导入', 23 | 'searchTable.operation.download': '下载', 24 | // columns 25 | 'searchTable.columns.index': '#', 26 | 'searchTable.columns.number': '集合编号', 27 | 'searchTable.columns.name': '集合名称', 28 | 'searchTable.columns.contentType': '内容体裁', 29 | 'searchTable.columns.filterType': '筛选方式', 30 | 'searchTable.columns.count': '内容量', 31 | 'searchTable.columns.createdTime': '创建时间', 32 | 'searchTable.columns.status': '状态', 33 | 'searchTable.columns.operations': '操作', 34 | 'searchTable.columns.operations.view': '查看', 35 | 36 | // size 37 | 'searchTable.size.mini': '迷你', 38 | 'searchTable.size.small': '偏小', 39 | 'searchTable.size.medium': '中等', 40 | 'searchTable.size.large': '偏大', 41 | // actions 42 | 'searchTable.actions.refresh': '刷新', 43 | 'searchTable.actions.density': '密度', 44 | 'searchTable.actions.columnSetting': '列设置', 45 | }; 46 | -------------------------------------------------------------------------------- /src/views/list/settings/index.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 183 | 184 | 189 | 190 | 234 | -------------------------------------------------------------------------------- /src/views/list/settings/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.cardList': 'Card List', 3 | 'cardList.tab.title.all': 'All', 4 | 'cardList.tab.title.content': 'Quality Inspection', 5 | 'cardList.tab.title.service': 'The service', 6 | 'cardList.tab.title.preset': 'Rules Preset', 7 | 'cardList.searchInput.placeholder': 'Search', 8 | 'cardList.enable': 'Enable', 9 | 'cardList.disable': 'Disable', 10 | 'cardList.content.delete': 'Delete', 11 | 'cardList.content.inspection': 'Inspection', 12 | 'cardList.content.action': 'Click Create Qc Content queue', 13 | 'cardList.service.open': 'Open', 14 | 'cardList.service.cancel': 'Cancel', 15 | 'cardList.service.renew': 'Contract of service', 16 | 'cardList.service.tag': 'Opened', 17 | 'cardList.service.expiresTag': 'Expired', 18 | 'cardList.preset.tag': 'Enable', 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/list/settings/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.list.cardList': '卡片列表', 3 | 'cardList.tab.title.all': '全部', 4 | 'cardList.tab.title.content': '内容质检', 5 | 'cardList.tab.title.service': '开通服务', 6 | 'cardList.tab.title.preset': '规则预置', 7 | 'cardList.searchInput.placeholder': '搜索', 8 | // 'cardList.statistic.enable': '已启用', 9 | // 'cardList.statistic.disable': '未启用', 10 | 'cardList.content.delete': '删除', 11 | 'cardList.content.inspection': '质检', 12 | 'cardList.content.action': '点击创建质检内容队列', 13 | 'cardList.service.open': '开通服务', 14 | 'cardList.service.cancel': '取消服务', 15 | 'cardList.service.renew': '续约服务', 16 | 'cardList.service.tag': '已开通', 17 | 'cardList.service.expiresTag': '已过期', 18 | 'cardList.preset.tag': '已启用', 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/not-found/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/views/thanks/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 94 | 95 | 100 | 101 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "baseUrl": ".", 12 | "paths": { 13 | "@/*": ["src/*"] 14 | }, 15 | "lib": ["es2020", "dom"], 16 | "skipLibCheck": true 17 | }, 18 | "include": ["src/**/*", "src/**/*.vue"], 19 | "exclude": ["node_modules"] 20 | } 21 | --------------------------------------------------------------------------------