init
This commit is contained in:
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
29
.env.development
Normal file
29
.env.development
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# 本地环境
|
||||||
|
ENV = development
|
||||||
|
|
||||||
|
# 测试ws
|
||||||
|
# VITE_API_WSS = 'wss://sockets.sxczgkj.com/wss'
|
||||||
|
|
||||||
|
# 正式ws
|
||||||
|
# VITE_API_WSS = 'wss://czgeatws.sxczgkj.com/wss'
|
||||||
|
|
||||||
|
# 本地ws
|
||||||
|
VITE_API_WSS = 'ws://192.168.1.42:2348'
|
||||||
|
|
||||||
|
# 正式 php
|
||||||
|
VITE_API_PHP_URL = 'https://newblockwlx.sxczgkj.cn/index.php/api'
|
||||||
|
|
||||||
|
# 测试 php 开票
|
||||||
|
# VITE_API_KP_URL = 'http://192.168.1.13:8888/api'
|
||||||
|
|
||||||
|
# 正式 php 开票
|
||||||
|
VITE_API_KP_URL = 'https://invoice.sxczgkj.cn/api'
|
||||||
|
|
||||||
|
# 本地调试连接
|
||||||
|
VITE_API_URL = 'http://192.168.1.42/'
|
||||||
|
|
||||||
|
# 线上测试
|
||||||
|
# VITE_API_URL = 'https://tapi.cashier.sxczgkj.cn'
|
||||||
|
|
||||||
|
# 线上正式
|
||||||
|
# VITE_API_URL = 'https://cashier.sxczgkj.com'
|
||||||
14
.env.production
Normal file
14
.env.production
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 线上环境
|
||||||
|
ENV = production
|
||||||
|
|
||||||
|
# 正式ws
|
||||||
|
VITE_API_WSS = 'wss://czgeatws.sxczgkj.com/wss'
|
||||||
|
|
||||||
|
# 正式 php
|
||||||
|
VITE_API_PHP_URL = 'https://newblockwlx.sxczgkj.cn/index.php/api'
|
||||||
|
|
||||||
|
# 正式 php 开票
|
||||||
|
VITE_API_KP_URL = 'https://invoice.sxczgkj.cn/api'
|
||||||
|
|
||||||
|
# 线上环境接口地址
|
||||||
|
VITE_API_URL = 'https://cashier.sxczgkj.com/'
|
||||||
29
.env.test
Normal file
29
.env.test
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# 测试环境
|
||||||
|
ENV = test
|
||||||
|
|
||||||
|
# 测试ws
|
||||||
|
VITE_API_WSS = 'ws://192.168.1.42:2348'
|
||||||
|
|
||||||
|
# 测试ws
|
||||||
|
# VITE_API_WSS = 'wss://sockets.sxczgkj.com/wss'
|
||||||
|
|
||||||
|
# 正式ws
|
||||||
|
# VITE_API_WSS = 'wss://czgeatws.sxczgkj.com/wss'
|
||||||
|
|
||||||
|
# 正式 php
|
||||||
|
VITE_API_PHP_URL = 'https://newblockwlx.sxczgkj.cn/index.php/api'
|
||||||
|
|
||||||
|
# 测试 php 开票
|
||||||
|
# VITE_API_KP_URL = 'http://192.168.1.13:8888/api'
|
||||||
|
|
||||||
|
# 正式 php 开票
|
||||||
|
VITE_API_KP_URL = 'https://invoice.sxczgkj.cn/api'
|
||||||
|
|
||||||
|
# 测试Java
|
||||||
|
# VITE_API_URL = 'https://fv901fw8033.vicp.fun/'
|
||||||
|
|
||||||
|
# 正式Java
|
||||||
|
# VITE_API_URL = 'https://cashier.sxczgkj.com/'
|
||||||
|
|
||||||
|
# 本地调试连接
|
||||||
|
VITE_API_URL = 'http://192.168.1.42/'
|
||||||
88
.eslintrc-auto-import.json
Normal file
88
.eslintrc-auto-import.json
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"globals": {
|
||||||
|
"Component": true,
|
||||||
|
"ComponentPublicInstance": true,
|
||||||
|
"ComputedRef": true,
|
||||||
|
"DirectiveBinding": true,
|
||||||
|
"EffectScope": true,
|
||||||
|
"ElLoading": true,
|
||||||
|
"ElMessage": true,
|
||||||
|
"ElMessageBox": true,
|
||||||
|
"ElNotification": true,
|
||||||
|
"ExtractDefaultPropTypes": true,
|
||||||
|
"ExtractPropTypes": true,
|
||||||
|
"ExtractPublicPropTypes": true,
|
||||||
|
"InjectionKey": true,
|
||||||
|
"MaybeRef": true,
|
||||||
|
"MaybeRefOrGetter": true,
|
||||||
|
"PropType": true,
|
||||||
|
"Ref": true,
|
||||||
|
"ShallowRef": true,
|
||||||
|
"Slot": true,
|
||||||
|
"Slots": true,
|
||||||
|
"VNode": true,
|
||||||
|
"WritableComputedRef": true,
|
||||||
|
"computed": true,
|
||||||
|
"createApp": true,
|
||||||
|
"customRef": true,
|
||||||
|
"defineAsyncComponent": true,
|
||||||
|
"defineComponent": true,
|
||||||
|
"effectScope": true,
|
||||||
|
"getCurrentInstance": true,
|
||||||
|
"getCurrentScope": true,
|
||||||
|
"getCurrentWatcher": true,
|
||||||
|
"h": true,
|
||||||
|
"inject": true,
|
||||||
|
"isProxy": true,
|
||||||
|
"isReactive": true,
|
||||||
|
"isReadonly": true,
|
||||||
|
"isRef": true,
|
||||||
|
"isShallow": true,
|
||||||
|
"markRaw": true,
|
||||||
|
"nextTick": true,
|
||||||
|
"onActivated": true,
|
||||||
|
"onBeforeMount": true,
|
||||||
|
"onBeforeRouteLeave": true,
|
||||||
|
"onBeforeRouteUpdate": true,
|
||||||
|
"onBeforeUnmount": true,
|
||||||
|
"onBeforeUpdate": true,
|
||||||
|
"onDeactivated": true,
|
||||||
|
"onErrorCaptured": true,
|
||||||
|
"onMounted": true,
|
||||||
|
"onRenderTracked": true,
|
||||||
|
"onRenderTriggered": true,
|
||||||
|
"onScopeDispose": true,
|
||||||
|
"onServerPrefetch": true,
|
||||||
|
"onUnmounted": true,
|
||||||
|
"onUpdated": true,
|
||||||
|
"onWatcherCleanup": true,
|
||||||
|
"provide": true,
|
||||||
|
"reactive": true,
|
||||||
|
"readonly": true,
|
||||||
|
"ref": true,
|
||||||
|
"resolveComponent": true,
|
||||||
|
"shallowReactive": true,
|
||||||
|
"shallowReadonly": true,
|
||||||
|
"shallowRef": true,
|
||||||
|
"toRaw": true,
|
||||||
|
"toRef": true,
|
||||||
|
"toRefs": true,
|
||||||
|
"toValue": true,
|
||||||
|
"triggerRef": true,
|
||||||
|
"unref": true,
|
||||||
|
"useAttrs": true,
|
||||||
|
"useCssModule": true,
|
||||||
|
"useCssVars": true,
|
||||||
|
"useId": true,
|
||||||
|
"useLink": true,
|
||||||
|
"useModel": true,
|
||||||
|
"useRoute": true,
|
||||||
|
"useRouter": true,
|
||||||
|
"useSlots": true,
|
||||||
|
"useTemplateRef": true,
|
||||||
|
"watch": true,
|
||||||
|
"watchEffect": true,
|
||||||
|
"watchPostEffect": true,
|
||||||
|
"watchSyncEffect": true
|
||||||
|
}
|
||||||
|
}
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
out
|
||||||
|
.DS_Store
|
||||||
|
.eslintcache
|
||||||
|
*.log*
|
||||||
3
.npmrc
Normal file
3
.npmrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||||
|
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||||
|
shamefully-hoist=true
|
||||||
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
out
|
||||||
|
dist
|
||||||
|
pnpm-lock.yaml
|
||||||
|
LICENSE.md
|
||||||
|
tsconfig.json
|
||||||
|
tsconfig.*.json
|
||||||
4
.prettierrc.yaml
Normal file
4
.prettierrc.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
singleQuote: true
|
||||||
|
semi: false
|
||||||
|
printWidth: 100
|
||||||
|
trailingComma: none
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["dbaeumer.vscode-eslint"]
|
||||||
|
}
|
||||||
39
.vscode/launch.json
vendored
Normal file
39
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug Main Process",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
|
||||||
|
"windows": {
|
||||||
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
|
||||||
|
},
|
||||||
|
"runtimeArgs": ["--sourcemap"],
|
||||||
|
"env": {
|
||||||
|
"REMOTE_DEBUGGING_PORT": "9222"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug Renderer Process",
|
||||||
|
"port": 9222,
|
||||||
|
"request": "attach",
|
||||||
|
"type": "chrome",
|
||||||
|
"webRoot": "${workspaceFolder}/src/renderer",
|
||||||
|
"timeout": 60000,
|
||||||
|
"presentation": {
|
||||||
|
"hidden": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Debug All",
|
||||||
|
"configurations": ["Debug Main Process", "Debug Renderer Process"],
|
||||||
|
"presentation": {
|
||||||
|
"order": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
README.md
Normal file
34
README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# kitchen-desktop
|
||||||
|
|
||||||
|
An Electron application with Vue
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For windows
|
||||||
|
$ pnpm build:win
|
||||||
|
|
||||||
|
# For macOS
|
||||||
|
$ pnpm build:mac
|
||||||
|
|
||||||
|
# For Linux
|
||||||
|
$ pnpm build:linux
|
||||||
|
```
|
||||||
20
addVersion.js
Normal file
20
addVersion.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//npm run build打包前执行此段代码
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
//返回package的json数据
|
||||||
|
function getPackageJson() {
|
||||||
|
let data = fs.readFileSync('./package.json');//fs读取文件
|
||||||
|
return JSON.parse(data);//转换为json对象
|
||||||
|
}
|
||||||
|
|
||||||
|
let packageData = getPackageJson();//获取package的json
|
||||||
|
let arr = packageData.version.split('.');//切割后的版本号数组
|
||||||
|
arr[2] = parseInt(arr[2]) + 1;
|
||||||
|
packageData.version = arr.join('.');//转换为以"."分割的字符串
|
||||||
|
//用packageData覆盖package.json内容
|
||||||
|
fs.writeFile(
|
||||||
|
'./package.json',
|
||||||
|
JSON.stringify(packageData, null, "\t"
|
||||||
|
),
|
||||||
|
(err) => { }
|
||||||
|
);
|
||||||
12
build/entitlements.mac.plist
Normal file
12
build/entitlements.mac.plist
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
build/icon.icns
Normal file
BIN
build/icon.icns
Normal file
Binary file not shown.
BIN
build/icon.ico
Normal file
BIN
build/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
BIN
build/icon.png
Normal file
BIN
build/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
44
electron-builder.yml
Normal file
44
electron-builder.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
appId: com.electron.app
|
||||||
|
productName: kitchen-desktop
|
||||||
|
directories:
|
||||||
|
buildResources: build
|
||||||
|
files:
|
||||||
|
- '!**/.vscode/*'
|
||||||
|
- '!src/*'
|
||||||
|
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||||
|
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||||
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
|
asarUnpack:
|
||||||
|
- resources/**
|
||||||
|
win:
|
||||||
|
executableName: kitchen-desktop
|
||||||
|
nsis:
|
||||||
|
artifactName: ${name}-${version}-setup.${ext}
|
||||||
|
shortcutName: ${productName}
|
||||||
|
uninstallDisplayName: ${productName}
|
||||||
|
createDesktopShortcut: always
|
||||||
|
mac:
|
||||||
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
|
extendInfo:
|
||||||
|
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||||
|
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
|
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
|
notarize: false
|
||||||
|
dmg:
|
||||||
|
artifactName: ${name}-${version}.${ext}
|
||||||
|
linux:
|
||||||
|
target:
|
||||||
|
- AppImage
|
||||||
|
- snap
|
||||||
|
- deb
|
||||||
|
maintainer: electronjs.org
|
||||||
|
category: Utility
|
||||||
|
appImage:
|
||||||
|
artifactName: ${name}-${version}.${ext}
|
||||||
|
npmRebuild: false
|
||||||
|
publish:
|
||||||
|
provider: generic
|
||||||
|
url: https://example.com/auto-updates
|
||||||
|
electronDownload:
|
||||||
|
mirror: https://npmmirror.com/mirrors/electron/
|
||||||
80
electron.vite.config.mjs
Normal file
80
electron.vite.config.mjs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { resolve } from 'path'
|
||||||
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||||
|
import { loadEnv } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
|
||||||
|
export default defineConfig(({ command, mode }) => {
|
||||||
|
const env = loadEnv(mode, process.cwd(), "")
|
||||||
|
|
||||||
|
const rendererSrc = resolve(__dirname, 'src/renderer/src')
|
||||||
|
|
||||||
|
return {
|
||||||
|
main: {
|
||||||
|
plugins: [externalizeDepsPlugin()]
|
||||||
|
},
|
||||||
|
preload: {
|
||||||
|
plugins: [externalizeDepsPlugin()]
|
||||||
|
},
|
||||||
|
renderer: {
|
||||||
|
// 保持并合并原 vite 配置(proxy、resolve、esbuild 等)
|
||||||
|
server: {
|
||||||
|
hmr: { enabled: true },
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: env.VITE_API_URL,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
|
},
|
||||||
|
'/php': {
|
||||||
|
target: env.VITE_API_PHP_URL,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/php/, ''),
|
||||||
|
},
|
||||||
|
'/kp': {
|
||||||
|
target: env.VITE_API_KP_URL,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/kp/, ''),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@renderer': rendererSrc,
|
||||||
|
'@': rendererSrc,
|
||||||
|
},
|
||||||
|
extensions: ['.vue', '.js', '.jsx', '.json'],
|
||||||
|
preserveSymlinks: true,
|
||||||
|
ignoreCase: false,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
// 自动导入 Composition API、vue-router 等,生成类型声明到 src/auto-imports.d.ts
|
||||||
|
AutoImport({
|
||||||
|
imports: [
|
||||||
|
'vue',
|
||||||
|
'vue-router',
|
||||||
|
{
|
||||||
|
'element-plus': [
|
||||||
|
'ElMessage',
|
||||||
|
'ElMessageBox',
|
||||||
|
'ElNotification',
|
||||||
|
'ElLoading'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
dts: 'src/auto-imports.d.ts',
|
||||||
|
// 生成 ESLint 全局变量配置,防止 ESLint 报未定义
|
||||||
|
eslintrc: {
|
||||||
|
enabled: true,
|
||||||
|
filepath: './.eslintrc-auto-import.json',
|
||||||
|
globalsPropValue: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
esbuild: {
|
||||||
|
drop: env.ENV == 'production' ? ['console'] : [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
30
eslint.config.mjs
Normal file
30
eslint.config.mjs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import eslintConfig from '@electron-toolkit/eslint-config'
|
||||||
|
import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier'
|
||||||
|
import eslintPluginVue from 'eslint-plugin-vue'
|
||||||
|
import vueParser from 'vue-eslint-parser'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{ ignores: ['**/node_modules', '**/dist', '**/out'] },
|
||||||
|
eslintConfig,
|
||||||
|
...eslintPluginVue.configs['flat/recommended'],
|
||||||
|
{
|
||||||
|
files: ['**/*.vue'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: vueParser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true
|
||||||
|
},
|
||||||
|
extraFileExtensions: ['.vue']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx,vue}'],
|
||||||
|
rules: {
|
||||||
|
'vue/require-default-prop': 'off',
|
||||||
|
'vue/multi-word-component-names': 'off'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
eslintConfigPrettier
|
||||||
|
]
|
||||||
29
jsconfig.json
Normal file
29
jsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
],
|
||||||
|
"@renderer/*": [
|
||||||
|
"src/renderer/src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"DOM"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.js",
|
||||||
|
"src/**/*.vue",
|
||||||
|
"src/auto-imports.d.ts", // 关键:包含自动导入的声明文件
|
||||||
|
"src/components.d.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
56
package.json
Normal file
56
package.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "yinshouke-kitchen",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "后厨专用软件",
|
||||||
|
"main": "./out/main/index.js",
|
||||||
|
"author": "example.com",
|
||||||
|
"homepage": "https://electron-vite.org",
|
||||||
|
"scripts": {
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "eslint --cache .",
|
||||||
|
"start": "electron-vite preview",
|
||||||
|
"dev": "electron-vite dev",
|
||||||
|
"build": "electron-vite build",
|
||||||
|
"build:test": "electron-vite build --mode test && electron-builder --win",
|
||||||
|
"postinstall": "electron-builder install-app-deps",
|
||||||
|
"build:unpack": "npm run build && electron-builder --dir",
|
||||||
|
"build:win": "node ./addVersion.js && vite build && npm run build && electron-builder --win",
|
||||||
|
"build:mac": "npm run build && electron-builder --mac",
|
||||||
|
"build:linux": "npm run build && electron-builder --linux"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
|
"element-plus": "^2.11.8",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"reconnecting-websocket": "^4.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@electron-toolkit/eslint-config": "^2.1.0",
|
||||||
|
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||||
|
"@vitejs/plugin-vue": "5.1.4",
|
||||||
|
"electron": "^38.1.2",
|
||||||
|
"electron-builder": "^25.1.8",
|
||||||
|
"electron-vite": "^4.0.1",
|
||||||
|
"eslint": "^9.36.0",
|
||||||
|
"eslint-plugin-vue": "^10.4.0",
|
||||||
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"sass": "^1.94.2",
|
||||||
|
"unplugin-auto-import": "^20.3.0",
|
||||||
|
"vite": "5.4.8",
|
||||||
|
"vue": "^3.5.21",
|
||||||
|
"vue-eslint-parser": "^10.2.0",
|
||||||
|
"vue-router": "4"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"electron",
|
||||||
|
"esbuild"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
4869
pnpm-lock.yaml
generated
Normal file
4869
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
resources/icon.png
Normal file
BIN
resources/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
9
src/auto-imports.d.ts
vendored
Normal file
9
src/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// This file is generated by unplugin-auto-import. If you don't see it after install+dev, the plugin will create it at runtime.
|
||||||
|
// You can keep this placeholder so editors recognize the path before generation.
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
declare module 'vue' {
|
||||||
|
export function ref<T = any>(value?: T): import('vue').Ref<T>
|
||||||
|
export function reactive<T = any>(value: T): T
|
||||||
|
export function onMounted(fn: () => void): void
|
||||||
|
}
|
||||||
108
src/main/index.js
Normal file
108
src/main/index.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { app, shell, BrowserWindow, ipcMain, screen } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
|
import icon from '../../resources/icon.png?asset'
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
// 创建主窗口:这是渲染进程的承载窗口
|
||||||
|
// 说明:窗口配置可根据产品需求调整尺寸、是否显示菜单栏、是否显示时机等。
|
||||||
|
// 注意:在 Electron 中尽量将不受信任的逻辑放到 preload 中并开启 sandbox(如可行),
|
||||||
|
// 以降低 Renderer 被注入恶意脚本时的攻击面。本项目为兼容性将 sandbox 设为 false。
|
||||||
|
// 开发环境下使用固定宽高,生产环境改为使用屏幕可用区域宽高
|
||||||
|
let winWidth = 1280
|
||||||
|
let winHeight = 670
|
||||||
|
if (!is.dev) {
|
||||||
|
try {
|
||||||
|
const { width, height } = screen.getPrimaryDisplay().workAreaSize
|
||||||
|
winWidth = width
|
||||||
|
winHeight = height
|
||||||
|
} catch (e) {
|
||||||
|
// 若获取屏幕信息失败则回退为默认值
|
||||||
|
console.warn('获取屏幕尺寸失败,使用默认窗口大小', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainWindow = new BrowserWindow({
|
||||||
|
width: winWidth,
|
||||||
|
height: winHeight,
|
||||||
|
show: false,
|
||||||
|
// 开发环境下显示菜单栏,生产环境下隐藏菜单栏
|
||||||
|
autoHideMenuBar: !is.dev,
|
||||||
|
...(process.platform === 'linux' ? { icon } : {}),
|
||||||
|
webPreferences: {
|
||||||
|
// preload 脚本用于在渲染进程加载前注入受控的桥接 API(推荐用于 IPC)
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
// sandbox 控制是否启用 Chromium 的沙箱(提高安全性但可能影响某些原生模块行为)
|
||||||
|
sandbox: false,
|
||||||
|
// 关闭 webSecurity 会禁用同源策略和部分安全校验(危险)。
|
||||||
|
// 按用户要求在桌面端允许所有请求时启用此配置。
|
||||||
|
webSecurity: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.on('ready-to-show', () => {
|
||||||
|
mainWindow.show()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 始终允许使用 F12 打开/关闭开发者工具(在任何环境都可用)
|
||||||
|
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||||
|
// 处理 F12 快捷键
|
||||||
|
if (input.key === 'F12') {
|
||||||
|
if (mainWindow && mainWindow.webContents) {
|
||||||
|
mainWindow.webContents.toggleDevTools()
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
shell.openExternal(details.url)
|
||||||
|
return { action: 'deny' }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 开发/生产加载逻辑:
|
||||||
|
// - 开发环境(is.dev)下,使用 Vite 提供的远程渲染地址(热重载、HMR)。
|
||||||
|
// - 生产环境下,加载打包后的本地 HTML 文件(更可靠且无需热重载)。
|
||||||
|
// 注意:ELECTRON_RENDERER_URL 由 electron-vite 在开发时注入,指向本地 dev server。
|
||||||
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
|
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当 Electron 初始化完成并准备创建窗口时执行(某些 API 必须在此之后使用)
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
// 在 Windows 上设置 App User Model ID,可支持通知、任务栏图标分组等功能
|
||||||
|
electronApp.setAppUserModelId('com.electron')
|
||||||
|
|
||||||
|
// 在开发环境中,绑定默认快捷键行为:F12 打开/关闭 DevTools,生产环境禁用刷新快捷键等
|
||||||
|
// 具体细节由 electron-toolkit 的 optimizer 帮助管理,以提升开发体验并避免生产环境误触。
|
||||||
|
app.on('browser-window-created', (_, window) => {
|
||||||
|
optimizer.watchWindowShortcuts(window)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 简单的 IPC 测试示例:主进程监听 ping 并在控制台打印 pong
|
||||||
|
// 说明:真实项目中请在 preload 中注册受控的 IPC 接口给渲染进程使用,避免直接暴露 ipcRenderer
|
||||||
|
ipcMain.on('ping', () => console.log('pong'))
|
||||||
|
|
||||||
|
// 创建主窗口并加载渲染内容
|
||||||
|
createWindow()
|
||||||
|
|
||||||
|
// macOS 特殊行为:当 Dock 图标被点击且没有窗口时,通常需要重新创建窗口
|
||||||
|
app.on('activate', function () {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 关闭应用的行为:当所有窗口关闭时退出应用(macOS 除外,macOS 通常在菜单中保留应用运行)
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 其他主进程逻辑可以放在此文件或单独模块中并在此处引入。
|
||||||
|
// 安全建议:
|
||||||
|
// - 尽量将渲染进程与主进程的职责分离,通过 preload 提供受控的桥接接口。
|
||||||
|
// - 在生产环境打开 CSP、安全头或 nodeIntegration 限制,减少攻击面。
|
||||||
20
src/preload/index.js
Normal file
20
src/preload/index.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { contextBridge } from 'electron'
|
||||||
|
import { electronAPI } from '@electron-toolkit/preload'
|
||||||
|
|
||||||
|
// Custom APIs for renderer
|
||||||
|
const api = {}
|
||||||
|
|
||||||
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
// renderer only if context isolation is enabled, otherwise
|
||||||
|
// just add to the DOM global.
|
||||||
|
if (process.contextIsolated) {
|
||||||
|
try {
|
||||||
|
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||||
|
contextBridge.exposeInMainWorld('api', api)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.electron = electronAPI
|
||||||
|
window.api = api
|
||||||
|
}
|
||||||
24
src/renderer/index.html
Normal file
24
src/renderer/index.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Electron</title>
|
||||||
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||||
|
<!--
|
||||||
|
为了允许 WebSocket 连接,需要为 connect-src 显式放行 ws: 或 wss:。
|
||||||
|
这里添加了 `connect-src 'self' ws: wss:`,表示允许当前源与任意 ws/wss 主机建立连接。
|
||||||
|
出于安全考虑,建议把 ws: wss: 替换为精确的主机或域名,例如:
|
||||||
|
connect-src 'self' wss://ws.example.com;
|
||||||
|
在生产环境中请尽量限制为可靠的主机。
|
||||||
|
-->
|
||||||
|
<meta http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'self' 'unsafe-inline' 'unsafe-eval' *; connect-src * ws: wss: http: https: data: blob: filesystem:; script-src * 'unsafe-inline' 'unsafe-eval'; style-src * 'unsafe-inline'; img-src * data: blob: filesystem:;" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
34
src/renderer/src/App.vue
Normal file
34
src/renderer/src/App.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import { useSocketStore } from '@/stores/socket';
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('WS URL:', import.meta.env.VITE_WS_URL);
|
||||||
|
|
||||||
|
// 重启或刷新防止ws丢失
|
||||||
|
const socketStore = useSocketStore(pinia)
|
||||||
|
socketStore.restoreFromStorage()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
42
src/renderer/src/api/account.js
Normal file
42
src/renderer/src/api/account.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import request from "@/utils/request.js";
|
||||||
|
|
||||||
|
const BASE_URL = '/account/admin'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户登录
|
||||||
|
* @param {*} data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function login(data) {
|
||||||
|
return request({
|
||||||
|
method: "post",
|
||||||
|
url: `${BASE_URL}/auth/login`,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码获取
|
||||||
|
* @param {*} data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function captcha(data) {
|
||||||
|
return request({
|
||||||
|
method: "get",
|
||||||
|
url: `${BASE_URL}/auth/captcha`,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
* @param {*} data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function logout(data) {
|
||||||
|
return request({
|
||||||
|
method: "post",
|
||||||
|
url: `${BASE_URL}/auth/logout`,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
14
src/renderer/src/api/market.js
Normal file
14
src/renderer/src/api/market.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import request from "@/utils/request.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智慧充值 配置信息获取
|
||||||
|
* @param {*} data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function shopRecharge(params) {
|
||||||
|
return request({
|
||||||
|
method: "get",
|
||||||
|
url: "/market/admin/shopRecharge",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
56
src/renderer/src/api/order.js
Normal file
56
src/renderer/src/api/order.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import request from "@/utils/request.js";
|
||||||
|
|
||||||
|
const BASE_URL = '/order/admin'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按台桌查看
|
||||||
|
* @param {*} params
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getKitchenTable(params) {
|
||||||
|
return request({
|
||||||
|
method: "get",
|
||||||
|
url: `${BASE_URL}/kitchen/getKitchenTable`,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按台桌查看 商品内容
|
||||||
|
* @param {*} params
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getKitchenTableFoods(params) {
|
||||||
|
return request({
|
||||||
|
method: "get",
|
||||||
|
url: `${BASE_URL}/kitchen/getKitchenTableFoods`,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按商品查看
|
||||||
|
* @param {*} params
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getKitchenFood(params) {
|
||||||
|
return request({
|
||||||
|
method: "get",
|
||||||
|
url: `${BASE_URL}/kitchen/getKitchenFood`,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 起菜, 上菜
|
||||||
|
* @param {*} data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function upOrderDetail(data) {
|
||||||
|
return request({
|
||||||
|
method: "put",
|
||||||
|
url: `${BASE_URL}/order/upOrderDetail`,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
16
src/renderer/src/api/product.js
Normal file
16
src/renderer/src/api/product.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import request from "@/utils/request.js";
|
||||||
|
|
||||||
|
const BASE_URL = '/product/admin/prod/'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商品分类列表
|
||||||
|
* @param {*} params
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function categoryList(params) {
|
||||||
|
return request({
|
||||||
|
method: "get",
|
||||||
|
url: `${BASE_URL}/category/list`,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
BIN
src/renderer/src/assets/logo_bg.png
Normal file
BIN
src/renderer/src/assets/logo_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 696 KiB |
BIN
src/renderer/src/assets/logo_text.png
Normal file
BIN
src/renderer/src/assets/logo_text.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
82
src/renderer/src/auto-imports.d.ts
vendored
Normal file
82
src/renderer/src/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
const EffectScope: typeof import('vue').EffectScope
|
||||||
|
const ElLoading: typeof import('element-plus').ElLoading
|
||||||
|
const ElMessage: typeof import('element-plus').ElMessage
|
||||||
|
const ElMessageBox: typeof import('element-plus').ElMessageBox
|
||||||
|
const ElNotification: typeof import('element-plus').ElNotification
|
||||||
|
const computed: typeof import('vue').computed
|
||||||
|
const createApp: typeof import('vue').createApp
|
||||||
|
const customRef: typeof import('vue').customRef
|
||||||
|
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
||||||
|
const defineComponent: typeof import('vue').defineComponent
|
||||||
|
const effectScope: typeof import('vue').effectScope
|
||||||
|
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||||
|
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||||
|
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||||
|
const h: typeof import('vue').h
|
||||||
|
const inject: typeof import('vue').inject
|
||||||
|
const isProxy: typeof import('vue').isProxy
|
||||||
|
const isReactive: typeof import('vue').isReactive
|
||||||
|
const isReadonly: typeof import('vue').isReadonly
|
||||||
|
const isRef: typeof import('vue').isRef
|
||||||
|
const isShallow: typeof import('vue').isShallow
|
||||||
|
const markRaw: typeof import('vue').markRaw
|
||||||
|
const nextTick: typeof import('vue').nextTick
|
||||||
|
const onActivated: typeof import('vue').onActivated
|
||||||
|
const onBeforeMount: typeof import('vue').onBeforeMount
|
||||||
|
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
|
||||||
|
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
|
||||||
|
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
||||||
|
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
||||||
|
const onDeactivated: typeof import('vue').onDeactivated
|
||||||
|
const onErrorCaptured: typeof import('vue').onErrorCaptured
|
||||||
|
const onMounted: typeof import('vue').onMounted
|
||||||
|
const onRenderTracked: typeof import('vue').onRenderTracked
|
||||||
|
const onRenderTriggered: typeof import('vue').onRenderTriggered
|
||||||
|
const onScopeDispose: typeof import('vue').onScopeDispose
|
||||||
|
const onServerPrefetch: typeof import('vue').onServerPrefetch
|
||||||
|
const onUnmounted: typeof import('vue').onUnmounted
|
||||||
|
const onUpdated: typeof import('vue').onUpdated
|
||||||
|
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
|
||||||
|
const provide: typeof import('vue').provide
|
||||||
|
const reactive: typeof import('vue').reactive
|
||||||
|
const readonly: typeof import('vue').readonly
|
||||||
|
const ref: typeof import('vue').ref
|
||||||
|
const resolveComponent: typeof import('vue').resolveComponent
|
||||||
|
const shallowReactive: typeof import('vue').shallowReactive
|
||||||
|
const shallowReadonly: typeof import('vue').shallowReadonly
|
||||||
|
const shallowRef: typeof import('vue').shallowRef
|
||||||
|
const toRaw: typeof import('vue').toRaw
|
||||||
|
const toRef: typeof import('vue').toRef
|
||||||
|
const toRefs: typeof import('vue').toRefs
|
||||||
|
const toValue: typeof import('vue').toValue
|
||||||
|
const triggerRef: typeof import('vue').triggerRef
|
||||||
|
const unref: typeof import('vue').unref
|
||||||
|
const useAttrs: typeof import('vue').useAttrs
|
||||||
|
const useCssModule: typeof import('vue').useCssModule
|
||||||
|
const useCssVars: typeof import('vue').useCssVars
|
||||||
|
const useId: typeof import('vue').useId
|
||||||
|
const useLink: typeof import('vue-router').useLink
|
||||||
|
const useModel: typeof import('vue').useModel
|
||||||
|
const useRoute: typeof import('vue-router').useRoute
|
||||||
|
const useRouter: typeof import('vue-router').useRouter
|
||||||
|
const useSlots: typeof import('vue').useSlots
|
||||||
|
const useTemplateRef: typeof import('vue').useTemplateRef
|
||||||
|
const watch: typeof import('vue').watch
|
||||||
|
const watchEffect: typeof import('vue').watchEffect
|
||||||
|
const watchPostEffect: typeof import('vue').watchPostEffect
|
||||||
|
const watchSyncEffect: typeof import('vue').watchSyncEffect
|
||||||
|
}
|
||||||
|
// for type re-export
|
||||||
|
declare global {
|
||||||
|
// @ts-ignore
|
||||||
|
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
|
import('vue')
|
||||||
|
}
|
||||||
17
src/renderer/src/components/icons/peopleIcon.vue
Normal file
17
src/renderer/src/components/icons/peopleIcon.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<svg t="1764238036312" class="icon" viewBox="0 0 1229 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
p-id="5896" :width="size" :height="size">
|
||||||
|
<path
|
||||||
|
d="M570.365088 585.913247a296.467113 296.467113 0 0 0 161.120466-262.988421 302.262813 302.262813 0 0 0-604.389257 0 296.535298 296.535298 0 0 0 161.120466 263.056606A424.245257 424.245257 0 0 0 0 985.407455a38.592545 38.592545 0 0 0 77.18509 0 352.037651 352.037651 0 0 1 704.075302 0 38.66073 38.66073 0 0 0 77.253275 0 425.199843 425.199843 0 0 0-288.216764-399.494208zM204.554126 322.924826a225.009539 225.009539 0 1 1 224.259507 221.736672A223.850398 223.850398 0 0 1 204.554126 322.924826z m0 0l754.054693 226.236863a296.330744 296.330744 0 0 0 141.687824-251.806129 300.62638 300.62638 0 0 0-301.921889-297.762623 37.978883 37.978883 0 1 0 0 75.957766 221.736673 221.736673 0 1 1 0 443.200606 38.115252 38.115252 0 1 0 0 76.162319 349.719371 349.719371 0 0 1 352.03765 346.991983 38.592545 38.592545 0 0 0 77.18509 0 423.972518 423.972518 0 0 0-268.784121-392.948476z m0 0"
|
||||||
|
fill="#666666" p-id="5897"></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 20
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
19
src/renderer/src/components/icons/scanIcon.vue
Normal file
19
src/renderer/src/components/icons/scanIcon.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<svg t="1764236030042" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
p-id="4757" data-spm-anchor-id="a313x.search_index.0.i2.d39c3a81bI6cex" :width="size" :height="size">
|
||||||
|
<path
|
||||||
|
d="M928 544 96 544c-17.664 0-32-14.336-32-32s14.336-32 32-32l832 0c17.696 0 32 14.336 32 32S945.696 544 928 544zM832 928l-192 0c-17.696 0-32-14.304-32-32s14.304-32 32-32l192 0c17.664 0 32-14.336 32-32l0-160c0-17.696 14.304-32 32-32s32 14.304 32 32l0 160C928 884.928 884.928 928 832 928zM352 928 192 928c-52.928 0-96-43.072-96-96l0-160c0-17.696 14.336-32 32-32s32 14.304 32 32l0 160c0 17.664 14.368 32 32 32l160 0c17.664 0 32 14.304 32 32S369.664 928 352 928zM128 384c-17.664 0-32-14.336-32-32L96 192c0-52.928 43.072-96 96-96l160 0c17.664 0 32 14.336 32 32s-14.336 32-32 32L192 160C174.368 160 160 174.368 160 192l0 160C160 369.664 145.664 384 128 384zM896 384c-17.696 0-32-14.336-32-32L864 192c0-17.632-14.336-32-32-32l-192 0c-17.696 0-32-14.336-32-32s14.304-32 32-32l192 0c52.928 0 96 43.072 96 96l0 160C928 369.664 913.696 384 896 384z"
|
||||||
|
fill="#333333" p-id="4758"></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 20
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
99
src/renderer/src/composables/useScanListener.js
Normal file
99
src/renderer/src/composables/useScanListener.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useScanListener
|
||||||
|
* 简单的扫码枪监听器(基于键盘事件,按键速率判断是否为扫码枪)
|
||||||
|
*
|
||||||
|
* options:
|
||||||
|
* - timeout: 两次按键超过此毫秒数则认为是新输入(默认 50ms)
|
||||||
|
* - minLength: 最小字符长度(默认 3)
|
||||||
|
* - ignoreIfFocus: 如果焦点在 input/textarea/contentEditable,则忽略(默认 true)
|
||||||
|
* - onScan: 扫描完成回调,参数为字符串
|
||||||
|
*/
|
||||||
|
export function useScanListener(options = {}) {
|
||||||
|
const {
|
||||||
|
timeout = 50,
|
||||||
|
minLength = 3,
|
||||||
|
ignoreIfFocus = true,
|
||||||
|
onScan = null,
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const lastScan = ref('')
|
||||||
|
let buffer = ''
|
||||||
|
let lastTime = 0
|
||||||
|
let timer = null
|
||||||
|
let started = false
|
||||||
|
|
||||||
|
function resetBuffer() {
|
||||||
|
buffer = ''
|
||||||
|
lastTime = 0
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyHandler(e) {
|
||||||
|
try {
|
||||||
|
const active = document.activeElement
|
||||||
|
const tag = active && active.tagName
|
||||||
|
if (ignoreIfFocus && (tag === 'INPUT' || tag === 'TEXTAREA' || active?.isContentEditable)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
if (lastTime && now - lastTime > timeout) {
|
||||||
|
// 超过间隔认为是新一次输入
|
||||||
|
buffer = ''
|
||||||
|
}
|
||||||
|
lastTime = now
|
||||||
|
|
||||||
|
// 结束按键(许多扫码枪以 Enter 结束)
|
||||||
|
if (e.key === 'Enter' || e.key === 'Return') {
|
||||||
|
if (buffer.length >= minLength) {
|
||||||
|
lastScan.value = buffer
|
||||||
|
if (typeof onScan === 'function') {
|
||||||
|
try { onScan(buffer) } catch (err) { console.error('onScan callback error', err) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resetBuffer()
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只收集可打印字符(长度为1)
|
||||||
|
if (e.key && e.key.length === 1) {
|
||||||
|
buffer += e.key
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止长时间不按键时残留,延迟清理
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
timer = setTimeout(() => resetBuffer(), Math.max(200, timeout * 4))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('scan keyHandler error', err)
|
||||||
|
resetBuffer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
if (started) return
|
||||||
|
window.addEventListener('keydown', keyHandler)
|
||||||
|
started = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
if (!started) return
|
||||||
|
window.removeEventListener('keydown', keyHandler)
|
||||||
|
resetBuffer()
|
||||||
|
started = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto start
|
||||||
|
start()
|
||||||
|
|
||||||
|
return {
|
||||||
|
lastScan,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/renderer/src/main.js
Normal file
44
src/renderer/src/main.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { useUserStore } from '@/stores/user';
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
import router from './router/index.js';
|
||||||
|
|
||||||
|
// 1. 引入 Element Plus 核心库和样式
|
||||||
|
import ElementPlus from 'element-plus';
|
||||||
|
import zhCn from "element-plus/es/locale/lang/zh-cn";
|
||||||
|
import 'element-plus/dist/index.css';
|
||||||
|
|
||||||
|
// 2. 可选:引入 Element Plus 图标库(全局注册)
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
// 1. 导入 Pinia 的创建函数
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
// 2. 可选:导入 Pinia 持久化插件
|
||||||
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||||
|
|
||||||
|
// 3. 创建 Pinia 实例
|
||||||
|
const pinia = createPinia();
|
||||||
|
// 4. 可选:注册持久化插件
|
||||||
|
pinia.use(piniaPluginPersistedstate);
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
app.use(router);
|
||||||
|
|
||||||
|
// 3. 全局注册 Element Plus
|
||||||
|
app.use(ElementPlus, { locale: zhCn });
|
||||||
|
|
||||||
|
// 4. 可选:全局注册所有图标(也可按需引入)
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(pinia); // 挂载 Pinia
|
||||||
|
|
||||||
|
// 初始化:挂载后立即校验 Token 并跳转
|
||||||
|
router.isReady().then(() => {
|
||||||
|
const userStore = useUserStore(); // 无需 require,直接使用
|
||||||
|
if (!userStore.token) {
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
app.mount('#app'); // 最后挂载应用
|
||||||
|
});
|
||||||
62
src/renderer/src/router/index.js
Normal file
62
src/renderer/src/router/index.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// 引入 Vue Router 核心方法
|
||||||
|
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
// 引入页面组件
|
||||||
|
import home from '@/views/home.vue';
|
||||||
|
import login from '@/views/login.vue';
|
||||||
|
|
||||||
|
// 路由规则数组
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: home
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
meta: {
|
||||||
|
requiresAuth: false
|
||||||
|
},
|
||||||
|
component: login
|
||||||
|
}
|
||||||
|
// 可选:配置 404 页面(需先创建 NotFound.vue 组件)
|
||||||
|
// {
|
||||||
|
// path: '/:pathMatch(.*)*',
|
||||||
|
// component: () => import('../views/NotFound.vue')
|
||||||
|
// }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 创建路由实例
|
||||||
|
const router = createRouter({
|
||||||
|
// 🔥 关键:Electron 中推荐使用 hash 模式(createWebHashHistory)
|
||||||
|
// 原因:Electron 渲染进程是本地文件加载,history 模式会导致刷新 404
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes // 传入路由规则
|
||||||
|
});
|
||||||
|
|
||||||
|
// 修复后的路由守卫
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const hasToken = !!userStore.token; // 转为布尔值,避免空字符串/undefined的判断误差
|
||||||
|
|
||||||
|
// 只有当目标页面需要登录,且无Token时,才跳转到登录页
|
||||||
|
if (to.meta.requiresAuth) {
|
||||||
|
if (hasToken) {
|
||||||
|
next(); // 有Token,正常放行
|
||||||
|
} else {
|
||||||
|
next({ path: "/login" }); // 无Token,跳登录页
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 目标页面无需登录(如登录页),直接放行
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
// 【可选】额外处理:已登录状态下访问登录页,自动跳转到首页
|
||||||
|
// if (to.path === "/login" && hasToken) {
|
||||||
|
// next({ path: "/" });
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导出路由实例,供 main.js 挂载
|
||||||
|
export default router;
|
||||||
344
src/renderer/src/stores/socket.js
Normal file
344
src/renderer/src/stores/socket.js
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import ReconnectingWebSocket from 'reconnecting-websocket'
|
||||||
|
import { useUserStore } from './user'
|
||||||
|
|
||||||
|
/*
|
||||||
|
socket store 简要说明(中文注释)
|
||||||
|
|
||||||
|
目的:
|
||||||
|
- 提供一个在整个应用中可复用的 WebSocket 管理层,支持自动重连、订阅管理和单例保证。
|
||||||
|
- 使用 `reconnecting-websocket` 实现自动重连策略,避免因短暂网络抖动导致的断线影响。
|
||||||
|
|
||||||
|
使用场景:
|
||||||
|
- 在任意组件中通过 `const socketStore = useSocketStore()` 获取实例并调用 `init`/`subscribe`/`send`。
|
||||||
|
- 在非组件模块(例如在 `main.js` 或 store 之外)中,如果没有全局注册 Pinia,可以传入 `pinia` 实例:`useSocketStore(pinia)`。
|
||||||
|
|
||||||
|
主要 API:
|
||||||
|
- init(url, protocols?, opts?):创建并绑定事件监听器,返回底层 ReconnectingWebSocket 实例。
|
||||||
|
- connect():尝试连接或返回现有连接。
|
||||||
|
- close(code, reason):主动关闭连接并清理单例实例。
|
||||||
|
- send(payload):发送消息(自动 JSON 序列化非字符串负载)。
|
||||||
|
- subscribe(fn):订阅到达的消息,返回 unsubscribe 函数。
|
||||||
|
- onOpen(fn)、onClose(fn):分别订阅连接打开/关闭回调,同样返回 unsubscribe 函数。
|
||||||
|
- restoreFromStorage():从 localStorage 恢复上次的 url 并尝试 init。
|
||||||
|
|
||||||
|
注意:store 本身不会在模块加载时自动创建 Pinia 实例或自动调用 init(避免副作用)。若需要自动恢复,请在应用入口处显式调用 `restoreFromStorage()`。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 模块级单例变量,用于保存底层 ReconnectingWebSocket 实例,确保全局唯一
|
||||||
|
let _rws = null
|
||||||
|
// 存储最终用于创建 RWS 的配置(会合并默认值和用户传入的 opts)
|
||||||
|
let _options = null
|
||||||
|
// 标记是否已经为当前 _rws 绑定了事件监听器,防止重复绑定导致回调被触发多次
|
||||||
|
let _listenersBound = false
|
||||||
|
// 心跳相关模块变量
|
||||||
|
let _heartbeatTimer = null
|
||||||
|
let _heartbeatTimeoutTimer = null
|
||||||
|
let _heartbeatOpts = null
|
||||||
|
let _missedPongs = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* createRws
|
||||||
|
* - 创建或返回已有的 ReconnectingWebSocket 单例
|
||||||
|
* - 参数:
|
||||||
|
* - url: websocket 地址,如 `wss://example.com/ws`(必须)
|
||||||
|
* - protocols: 可选的子协议数组
|
||||||
|
* - opts: 透传给 ReconnectingWebSocket 的选项(例如 connectionTimeout、maxRetries 等)
|
||||||
|
*/
|
||||||
|
function createRws(url, protocols = [], opts = {}) {
|
||||||
|
// 如果已经存在实例,直接返回(单例保证)
|
||||||
|
if (_rws) return _rws
|
||||||
|
|
||||||
|
const defaultOpts = {
|
||||||
|
// 在浏览器运行时使用全局 WebSocket
|
||||||
|
WebSocket: WebSocket,
|
||||||
|
connectionTimeout: 4000,
|
||||||
|
// 无限重试,直到手动 close
|
||||||
|
maxRetries: Infinity,
|
||||||
|
// 如需自定义重连间隔等策略,可在 opts 中传入
|
||||||
|
// reconnectInterval: 2000
|
||||||
|
}
|
||||||
|
|
||||||
|
_options = Object.assign({}, defaultOpts, opts)
|
||||||
|
_rws = new ReconnectingWebSocket(url, protocols, _options)
|
||||||
|
return _rws
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSocketStore = defineStore('socket', {
|
||||||
|
state: () => ({
|
||||||
|
// 当前使用的 websocket 地址(由 init 设置)
|
||||||
|
url: '',
|
||||||
|
// 子协议
|
||||||
|
protocols: null,
|
||||||
|
// 是否已连接(不保证数据收发正常,仅代表 open/close 状态)
|
||||||
|
connected: false,
|
||||||
|
// 最近一次收到的消息(解析成功则为对象/数组,否则为原始字符串)
|
||||||
|
lastMessage: null,
|
||||||
|
// 内部维护的订阅集合,key 为随机 id,value 为回调函数
|
||||||
|
_subs: {},
|
||||||
|
_openSubs: {},
|
||||||
|
_closeSubs: {},
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
/**
|
||||||
|
* init
|
||||||
|
* - 初始化 websocket:保存 url、持久化 url、创建底层 rws 实例并绑定事件
|
||||||
|
* - 注意:此函数应在应用初始化或用户登录后调用(当需要带 token 的 url)
|
||||||
|
*/
|
||||||
|
init(url, protocols = [], opts = {}) {
|
||||||
|
if (!url) throw new Error('socket init requires url')
|
||||||
|
this.url = url
|
||||||
|
this.protocols = protocols
|
||||||
|
|
||||||
|
// 将 url 持久化到 localStorage,刷新后可以通过 restoreFromStorage 恢复
|
||||||
|
try { localStorage.setItem('socket:url', url) } catch (e) { /* 在某些浏览器隐身模式下可能失败 */ }
|
||||||
|
|
||||||
|
// 如果已有实例且与当前 url 不同,则先关闭旧连接,避免多个连接同时存在
|
||||||
|
if (_rws && _rws.url && _rws.url !== url) {
|
||||||
|
try { _rws.close() } catch (e) { /* ignore */ }
|
||||||
|
_rws = null
|
||||||
|
_listenersBound = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const rws = createRws(url, protocols, opts)
|
||||||
|
|
||||||
|
// 仅为当前实例绑定一次事件监听,防止多次 init() 导致重复回调
|
||||||
|
if (!_listenersBound) {
|
||||||
|
// 连接打开事件:更新状态并通知所有 onOpen 订阅者
|
||||||
|
rws.addEventListener('open', () => {
|
||||||
|
this.connected = true
|
||||||
|
// 注意:不在这里隐式调用 send(),避免在多个地方重复发送初始化消息
|
||||||
|
Object.values(this._openSubs).forEach(fn => { try { fn() } catch (e) { console.error(e) } })
|
||||||
|
})
|
||||||
|
|
||||||
|
// 连接关闭事件:更新状态并通知所有 onClose 订阅者
|
||||||
|
rws.addEventListener('close', () => {
|
||||||
|
this.connected = false
|
||||||
|
Object.values(this._closeSubs).forEach(fn => { try { fn() } catch (e) { console.error(e) } })
|
||||||
|
})
|
||||||
|
|
||||||
|
// 消息到达事件:尝试解析 JSON(若失败则保留原始字符串),并通知所有 message 订阅者
|
||||||
|
rws.addEventListener('message', (ev) => {
|
||||||
|
let data = ev.data
|
||||||
|
try { data = JSON.parse(ev.data) } catch (e) { /* 若非 JSON 则保留原始字符串 */ }
|
||||||
|
this.lastMessage = data
|
||||||
|
|
||||||
|
// 识别 pong 响应(常见约定:字符串 'pong' 或对象中包含 type === 'pong' 或 operate_type === 'pong')
|
||||||
|
const isPong = (data === 'pong') || (data && (data.type === 'pong' || data.operate_type === 'pong'))
|
||||||
|
if (isPong) {
|
||||||
|
// 收到 pong 则重置未响应计数并清除超时计时器
|
||||||
|
_missedPongs = 0
|
||||||
|
if (_heartbeatTimeoutTimer) {
|
||||||
|
clearTimeout(_heartbeatTimeoutTimer)
|
||||||
|
_heartbeatTimeoutTimer = null
|
||||||
|
}
|
||||||
|
// 不把 pong 当作普通消息继续分发
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果消息携带 msg_id,回执给服务器(避免丢失)
|
||||||
|
try { if (data && data.msg_id) this.sendMsgId(data.msg_id) } catch (e) { console.error(e) }
|
||||||
|
|
||||||
|
Object.values(this._subs).forEach(fn => { try { fn(data) } catch (e) { console.error(e) } })
|
||||||
|
})
|
||||||
|
|
||||||
|
_listenersBound = true
|
||||||
|
// 如果外部传入了心跳配置,启动心跳
|
||||||
|
const hb = opts && opts.heartbeat
|
||||||
|
if (hb) {
|
||||||
|
// 存一份便于后续 stop/restart
|
||||||
|
_heartbeatOpts = Object.assign({ interval: 30000, timeout: 10000, maxMissed: 2, message: { type: 'ping' } }, hb)
|
||||||
|
try { this.startHeartbeat(_heartbeatOpts) } catch (e) { console.warn('startHeartbeat failed', e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储最终 options,以便后续 connect() 使用
|
||||||
|
_options = Object.assign({}, _options || {}, opts)
|
||||||
|
|
||||||
|
return rws
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* connect
|
||||||
|
* - 如果未指定 url,则尝试从 localStorage 恢复
|
||||||
|
* - 如果已有打开的连接则直接返回
|
||||||
|
*/
|
||||||
|
connect() {
|
||||||
|
if (!this.url) {
|
||||||
|
const saved = localStorage.getItem('socket:url')
|
||||||
|
if (saved) this.init(saved)
|
||||||
|
else throw new Error('no socket url to connect')
|
||||||
|
}
|
||||||
|
if (_rws && _rws.readyState === WebSocket.OPEN) return _rws
|
||||||
|
return createRws(this.url, this.protocols, _options)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* close
|
||||||
|
* - 主动关闭连接并清理模块级单例
|
||||||
|
* - 参数:close code(默认 1000 正常关闭)和 reason
|
||||||
|
*/
|
||||||
|
close(code = 1000, reason = 'client close') {
|
||||||
|
if (_rws) {
|
||||||
|
_rws.close(code, reason)
|
||||||
|
_rws = null
|
||||||
|
// 清除监听绑定标记,允许后续重新绑定新的实例
|
||||||
|
_listenersBound = false
|
||||||
|
// 停止心跳
|
||||||
|
try { this.stopHeartbeat() } catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
this.connected = false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* send
|
||||||
|
* - 发送消息到服务器,payload 可以是字符串或对象(对象会被 JSON.stringify)
|
||||||
|
* payload 格式 { operate_type: 'init , data: {...}}
|
||||||
|
*/
|
||||||
|
send(payload = { operate_type: 'init', data: { table_code: '' } }) {
|
||||||
|
if (!_rws) throw new Error('socket not initialized')
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
operate_type: 'init',
|
||||||
|
data: {
|
||||||
|
table_code: ''
|
||||||
|
},
|
||||||
|
...payload
|
||||||
|
}
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
let sendData = {
|
||||||
|
type: 'manage',
|
||||||
|
account: userStore.shopInfo.id,
|
||||||
|
operate_type: params.operate_type,
|
||||||
|
table_code: params.data.table_code,
|
||||||
|
shop_id: userStore.shopInfo.id
|
||||||
|
}
|
||||||
|
const data = typeof sendData === 'string' ? sendData : JSON.stringify(sendData)
|
||||||
|
_rws.send(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* startHeartbeat
|
||||||
|
* - 启动应用层心跳(通过发送自定义 ping 消息),并在超时/未收到 pong 时触发重连
|
||||||
|
* - opts: { interval, timeout, maxMissed, message }
|
||||||
|
*/
|
||||||
|
startHeartbeat(opts = {}) {
|
||||||
|
// 合并默认配置
|
||||||
|
const conf = Object.assign({ interval: 10000, timeout: 10000, maxMissed: 2, message: { type: "ping_interval", set: "manage" } }, opts)
|
||||||
|
_heartbeatOpts = conf
|
||||||
|
|
||||||
|
// 停止已有心跳
|
||||||
|
this.stopHeartbeat()
|
||||||
|
|
||||||
|
// 定时发送 ping
|
||||||
|
_heartbeatTimer = setInterval(() => {
|
||||||
|
try {
|
||||||
|
if (!_rws || _rws.readyState !== WebSocket.OPEN) return
|
||||||
|
// 发送心跳消息
|
||||||
|
const hbMsg = typeof conf.message === 'string' ? conf.message : JSON.stringify(conf.message)
|
||||||
|
_rws.send(hbMsg)
|
||||||
|
|
||||||
|
// 等待 pong 的超时计时器
|
||||||
|
if (_heartbeatTimeoutTimer) clearTimeout(_heartbeatTimeoutTimer)
|
||||||
|
_heartbeatTimeoutTimer = setTimeout(() => {
|
||||||
|
_missedPongs += 1
|
||||||
|
console.warn(`[socket] missed pong ${_missedPongs}/${conf.maxMissed}`)
|
||||||
|
if (_missedPongs >= conf.maxMissed) {
|
||||||
|
// 达到最大未响应次数,主动关闭底层连接以触发重连
|
||||||
|
try { _rws.close() } catch (e) { console.error(e) }
|
||||||
|
}
|
||||||
|
}, conf.timeout)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('heartbeat send error', e)
|
||||||
|
}
|
||||||
|
}, conf.interval)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stopHeartbeat
|
||||||
|
*/
|
||||||
|
stopHeartbeat() {
|
||||||
|
if (_heartbeatTimer) {
|
||||||
|
clearInterval(_heartbeatTimer)
|
||||||
|
_heartbeatTimer = null
|
||||||
|
}
|
||||||
|
if (_heartbeatTimeoutTimer) {
|
||||||
|
clearTimeout(_heartbeatTimeoutTimer)
|
||||||
|
_heartbeatTimeoutTimer = null
|
||||||
|
}
|
||||||
|
_missedPongs = 0
|
||||||
|
_heartbeatOpts = null
|
||||||
|
},
|
||||||
|
// 回应msg_id
|
||||||
|
sendMsgId(msg_id) {
|
||||||
|
if (!_rws) throw new Error('socket not initialized')
|
||||||
|
let sendData = {
|
||||||
|
type: 'receipt',
|
||||||
|
msg_id: msg_id
|
||||||
|
}
|
||||||
|
const data = typeof sendData === 'string' ? sendData : JSON.stringify(sendData)
|
||||||
|
_rws.send(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* subscribe
|
||||||
|
* - 订阅所有到达的消息,返回一个 unsubscribe 函数
|
||||||
|
* - 订阅函数签名:fn(message)
|
||||||
|
*/
|
||||||
|
subscribe(fn) {
|
||||||
|
if (typeof fn !== 'function') throw new Error('subscribe requires a function')
|
||||||
|
const id = Date.now().toString(36) + Math.random().toString(36).slice(2)
|
||||||
|
this._subs[id] = fn
|
||||||
|
return () => { delete this._subs[id] }
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onOpen
|
||||||
|
* - 监听连接打开事件,同样返回 unsubscribe
|
||||||
|
*/
|
||||||
|
onOpen(fn) {
|
||||||
|
if (typeof fn !== 'function') return () => { }
|
||||||
|
const id = Date.now().toString(36) + Math.random().toString(36).slice(2)
|
||||||
|
this._openSubs[id] = fn
|
||||||
|
return () => { delete this._openSubs[id] }
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onClose
|
||||||
|
* - 监听连接关闭事件,返回 unsubscribe
|
||||||
|
*/
|
||||||
|
onClose(fn) {
|
||||||
|
if (typeof fn !== 'function') return () => { }
|
||||||
|
const id = Date.now().toString(36) + Math.random().toString(36).slice(2)
|
||||||
|
this._closeSubs[id] = fn
|
||||||
|
return () => { delete this._closeSubs[id] }
|
||||||
|
},
|
||||||
|
|
||||||
|
// 手动恢复上次连接(比如页面刷新后)
|
||||||
|
restoreFromStorage() {
|
||||||
|
const saved = localStorage.getItem('socket:url')
|
||||||
|
if (saved) {
|
||||||
|
this.init(saved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模块级的自动恢复提示(但不自动创建 store 实例以避免副作用)
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('socket:url')
|
||||||
|
if (saved) {
|
||||||
|
// 延迟到微任务,以避免模块初始化顺序问题
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
// 注意:此处不自动调用 useSocketStore(),因为在模块加载阶段可能没有注册全局 pinia。
|
||||||
|
// 如果希望页面刷新后自动重连,请在应用入口(如 main.js)显式调用:
|
||||||
|
// const socketStore = useSocketStore(pinia)
|
||||||
|
// socketStore.restoreFromStorage()
|
||||||
|
const storeModule = null // placeholder: do not auto-create Pinia store here
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
75
src/renderer/src/stores/user.js
Normal file
75
src/renderer/src/stores/user.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// 导入 Pinia 的仓库创建函数
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { login as apiLogin, logout as apiLogout } from '@/api/account.js'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', {
|
||||||
|
state: () => ({
|
||||||
|
token: '',
|
||||||
|
shopInfo: '',
|
||||||
|
shopStaff: ''
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async login(params) {
|
||||||
|
try {
|
||||||
|
const res = await apiLogin(params)
|
||||||
|
if (res.shopInfo.registerType == 'after') {
|
||||||
|
this.token = res.tokenInfo.tokenValue
|
||||||
|
this.shopInfo = res.shopInfo
|
||||||
|
// 登录员工
|
||||||
|
if (res.loginType == 1) this.shopStaff = res.shopStaff
|
||||||
|
// async 函数直接返回值即可 resolve
|
||||||
|
return res
|
||||||
|
} else {
|
||||||
|
ElMessage.error('先下单模式下不可登录,请联系管理员')
|
||||||
|
// 抛出错误使调用方能捕获到失败状态
|
||||||
|
throw new Error('先下单模式下不可登录')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
// 向上抛出,确保调用方能收到 rejected 状态
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
logout() {
|
||||||
|
ElMessageBox.confirm('确定要退出登录吗?')
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
await apiLogout()
|
||||||
|
// 登出成功后尝试关闭 websocket(若已存在)
|
||||||
|
// try {
|
||||||
|
// const mod = await import('@/stores/socket')
|
||||||
|
// const socketStore = mod.useSocketStore()
|
||||||
|
// socketStore.close()
|
||||||
|
// } catch (e) {
|
||||||
|
// console.warn('close socket failed', e)
|
||||||
|
// }
|
||||||
|
|
||||||
|
this.token = ''
|
||||||
|
this.shopInfo = ''
|
||||||
|
this.shopStaff = ''
|
||||||
|
|
||||||
|
ElMessage.success('已退出登录')
|
||||||
|
// 避免在 store 顶层直接导入 router(会造成循环依赖或在模块初始化时为 undefined)
|
||||||
|
const { default: router } = await import('@/router')
|
||||||
|
router.replace({ name: 'login' })
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => { })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 核心:开启持久化,确保 Token 保存在 localStorage
|
||||||
|
persist: {
|
||||||
|
enabled: true,
|
||||||
|
strategies: [
|
||||||
|
{
|
||||||
|
key: 'kitchen-user',
|
||||||
|
storage: localStorage,
|
||||||
|
paths: ['token', 'shopInfo', 'shopStaff'] // 持久化指定状态
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
107
src/renderer/src/utils/index.js
Normal file
107
src/renderer/src/utils/index.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算当前时间与传入时间的时间差,并按规则格式化
|
||||||
|
* @param {string|number|Date} targetTime - 传入的目标时间(支持字符串、时间戳、Date对象)
|
||||||
|
* @returns {string} 格式化后的时间差(≤1小时:MM:ss;>1小时:HH:MM:ss)| 错误提示
|
||||||
|
*/
|
||||||
|
export const formatTimeDiff = (targetTime) => {
|
||||||
|
// 1. 校验传入时间的有效性
|
||||||
|
const target = dayjs(targetTime);
|
||||||
|
if (!target.isValid()) {
|
||||||
|
return '00:00'; // 无效时间返回提示
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 计算当前时间与目标时间的**秒级总差值**(当前时间 - 传入时间)
|
||||||
|
const diffSeconds = dayjs().diff(target, 'second');
|
||||||
|
// 处理差值为负数的情况(传入时间在当前时间之后,差值为负)
|
||||||
|
const absDiffSeconds = Math.abs(diffSeconds);
|
||||||
|
|
||||||
|
// 3. 按秒数分解为 小时、分钟、秒
|
||||||
|
const hours = Math.floor(absDiffSeconds / 3600); // 1小时=3600秒
|
||||||
|
const minutes = Math.floor((absDiffSeconds % 3600) / 60);
|
||||||
|
const seconds = absDiffSeconds % 60;
|
||||||
|
|
||||||
|
// 4. 补零函数:确保单个数字补0(如 5 → 05)
|
||||||
|
const padZero = (num) => num.toString().padStart(2, '0');
|
||||||
|
|
||||||
|
// 5. 根据是否超过1小时,返回不同格式
|
||||||
|
if (hours > 0) {
|
||||||
|
// >1小时:HH:MM:ss
|
||||||
|
return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}`;
|
||||||
|
} else {
|
||||||
|
// ≤1小时:MM:ss
|
||||||
|
return `${padZero(minutes)}:${padZero(seconds)}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断时分秒字符串是否大于指定分钟数
|
||||||
|
* @param {string} timeStr - 时分秒字符串,如 "26:30:40"
|
||||||
|
* @param {number} [thresholdMinutes=15] - 分钟阈值,默认15分钟
|
||||||
|
* @returns {boolean} 大于阈值返回true,否则false(格式错误返回false)
|
||||||
|
*/
|
||||||
|
export const isMoreThanSpecifiedMinutes = (timeStr, thresholdMinutes = 15) => {
|
||||||
|
// 1. 按冒号分割时分秒并转换为数字
|
||||||
|
const [hours, minutes, seconds] = timeStr.split(':').map(Number);
|
||||||
|
|
||||||
|
// 2. 校验时分秒格式是否合法
|
||||||
|
if (
|
||||||
|
isNaN(hours) ||
|
||||||
|
isNaN(minutes) ||
|
||||||
|
isNaN(seconds) ||
|
||||||
|
hours < 0 ||
|
||||||
|
minutes < 0 || minutes >= 60 ||
|
||||||
|
seconds < 0 || seconds >= 60
|
||||||
|
) {
|
||||||
|
console.error('时分秒格式错误,示例:"26:30:40"');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 转换为总秒数:小时*3600 + 分钟*60 + 秒
|
||||||
|
const totalSeconds = hours * 3600 + minutes * 60 + seconds;
|
||||||
|
|
||||||
|
// 4. 将分钟阈值转换为总秒数,进行比较
|
||||||
|
const thresholdSeconds = thresholdMinutes * 60;
|
||||||
|
|
||||||
|
return totalSeconds > thresholdSeconds;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算时分秒字符串超出指定分钟阈值的时长(返回HH:MM:SS格式)
|
||||||
|
* @param {string} timeStr - 时分秒字符串,如 "26:30:40"
|
||||||
|
* @param {number} [thresholdMinutes=15] - 分钟阈值,默认15分钟
|
||||||
|
* @returns {string} 超时时长(HH:MM:SS),未超时返回"00:00:00",格式错误返回"格式错误"
|
||||||
|
*/
|
||||||
|
export const calculateTimeoutDuration = (timeStr, thresholdMinutes = 15) => {
|
||||||
|
// 1. 按冒号分割时分秒并转换为数字
|
||||||
|
const [hours, minutes, seconds] = timeStr.split(':').map(Number);
|
||||||
|
|
||||||
|
// 2. 校验时分秒格式合法性
|
||||||
|
if (
|
||||||
|
isNaN(hours) ||
|
||||||
|
isNaN(minutes) ||
|
||||||
|
isNaN(seconds) ||
|
||||||
|
hours < 0 ||
|
||||||
|
minutes < 0 || minutes >= 60 ||
|
||||||
|
seconds < 0 || seconds >= 60
|
||||||
|
) {
|
||||||
|
console.error('时分秒格式错误,示例:"26:30:40"');
|
||||||
|
return '格式错误';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 转换为总秒数
|
||||||
|
const totalSeconds = hours * 3600 + minutes * 60 + seconds;
|
||||||
|
// 4. 计算阈值对应的总秒数
|
||||||
|
const thresholdSeconds = thresholdMinutes * 60;
|
||||||
|
// 5. 计算超时秒数(差值≤0则未超时)
|
||||||
|
const timeoutSeconds = Math.max(totalSeconds - thresholdSeconds, 0);
|
||||||
|
|
||||||
|
// 6. 将超时秒数转换回HH:MM:SS格式(补零处理)
|
||||||
|
const padZero = (num) => num.toString().padStart(2, '0');
|
||||||
|
const timeoutHours = padZero(Math.floor(timeoutSeconds / 3600));
|
||||||
|
const timeoutMins = padZero(Math.floor((timeoutSeconds % 3600) / 60));
|
||||||
|
const timeoutSecs = padZero(timeoutSeconds % 60);
|
||||||
|
|
||||||
|
return `${timeoutHours}:${timeoutMins}:${timeoutSecs}`;
|
||||||
|
};
|
||||||
77
src/renderer/src/utils/request.js
Normal file
77
src/renderer/src/utils/request.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
|
||||||
|
const service = axios.create({
|
||||||
|
baseURL:
|
||||||
|
import.meta.env.MODE == "development"
|
||||||
|
? "/api/"
|
||||||
|
: import.meta.env.VITE_API_URL,
|
||||||
|
// withCredentials: true, // 跨域请求时发送 cookies
|
||||||
|
timeout: 20000, // 请求超时
|
||||||
|
});
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
service.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 在发送请求之前做些什么 token
|
||||||
|
config.headers["platformType"] = "WEB";
|
||||||
|
if (userStore.token) {
|
||||||
|
// 让每个请求携带 token
|
||||||
|
// ['X-Token'] 是自定义标题键
|
||||||
|
// 请根据实际情况修改
|
||||||
|
config.headers["token"] = userStore.token;
|
||||||
|
if (userStore.shopInfo && userStore.shopInfo.id) {
|
||||||
|
config.headers["shopId"] = userStore.shopInfo.id;
|
||||||
|
}
|
||||||
|
// config.headers["loginName"] = useStorage.get("userInfo").loginName;
|
||||||
|
// config.headers['Content-Type'] = 'application/json'
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// 处理请求错误
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
service.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
// 对响应数据做点什么
|
||||||
|
if (+response.status === 200) {
|
||||||
|
if (+response.data.code == 200) {
|
||||||
|
return response.data.data;
|
||||||
|
} else if (+response.data.code == 501) {
|
||||||
|
useStorage.del("token");
|
||||||
|
useStorage.del("userInfo");
|
||||||
|
useStorage.del("shopInfo");
|
||||||
|
useStorage.del("douyin");
|
||||||
|
ElMessage.error("登录已过期,请重新登录");
|
||||||
|
window.location.reload();
|
||||||
|
return Promise.reject("登录已过期,请重新登录");
|
||||||
|
} else {
|
||||||
|
// 响应错误
|
||||||
|
ElMessage.error(response.data.msg);
|
||||||
|
return Promise.reject(response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// 对响应错误做点什么
|
||||||
|
if (error.message.indexOf("timeout") != -1) {
|
||||||
|
ElMessage.error("网络超时");
|
||||||
|
} else if (error.message == "Network Error") {
|
||||||
|
ElMessage.error("网络连接错误");
|
||||||
|
} else {
|
||||||
|
console.log(error);
|
||||||
|
if (error.response.data) ElMessage.error(error.response.statusText);
|
||||||
|
else ElMessage.error("接口路径找不到");
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default service;
|
||||||
42
src/renderer/src/utils/tableStatusList.js
Normal file
42
src/renderer/src/utils/tableStatusList.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
label: "未绑定",
|
||||||
|
type: "unbound",
|
||||||
|
color: "#909090",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "空闲",
|
||||||
|
type: "idle",
|
||||||
|
color: "#187CAA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "点餐中",
|
||||||
|
type: "ordering",
|
||||||
|
color: "#46AEA4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "未结账",
|
||||||
|
type: "unsettled",
|
||||||
|
color: "#DD3F41",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "支付中",
|
||||||
|
type: "paying",
|
||||||
|
color: "#909090",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "待清台",
|
||||||
|
type: "settled ",
|
||||||
|
color: "#FF9500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "关台",
|
||||||
|
type: "closed",
|
||||||
|
color: "#DDDDDD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "预定",
|
||||||
|
type: "subscribe",
|
||||||
|
color: "#58B22C",
|
||||||
|
},
|
||||||
|
];
|
||||||
15
src/renderer/src/utils/useStorage.js
Normal file
15
src/renderer/src/utils/useStorage.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
export default {
|
||||||
|
get(key) {
|
||||||
|
return JSON.parse(localStorage.getItem(key))
|
||||||
|
},
|
||||||
|
set(key, value) {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value))
|
||||||
|
},
|
||||||
|
del(key) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
localStorage.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
650
src/renderer/src/views/home.vue
Normal file
650
src/renderer/src/views/home.vue
Normal file
@@ -0,0 +1,650 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header_wrap">
|
||||||
|
<div class="btn" @click="userStore.logout()">
|
||||||
|
<el-icon>
|
||||||
|
<SwitchButton />
|
||||||
|
</el-icon>
|
||||||
|
<el-text>退出</el-text>
|
||||||
|
</div>
|
||||||
|
<div class="title"></div>
|
||||||
|
<div class="name">
|
||||||
|
<div class="dot" :class="{ online: socketStore.connected }"></div>
|
||||||
|
<el-text>
|
||||||
|
{{ userStore.shopStaff.name || userStore.shopInfo.shopName }}
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab_wrap">
|
||||||
|
<div class="left">
|
||||||
|
<div class="item" v-for="(item, index) in checkTypeList" :key="index"
|
||||||
|
@click="queryForm.tableName = ''; checkTypeHandle(item)">
|
||||||
|
<el-text :type="checkType == item.value ? 'primary' : ''">
|
||||||
|
{{ item.label }}
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right" @click="showScanTipsHandle">
|
||||||
|
<scanIcon />
|
||||||
|
<el-text>扫码出菜</el-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="search_wrap">
|
||||||
|
<div class="left">
|
||||||
|
<el-form inline @submit.prevent>
|
||||||
|
<el-form-item :label="checkType == 1 ? '台桌查找' : '菜品查找'">
|
||||||
|
<el-input v-model="queryForm.tableName" :placeholder="checkType == 1 ? '请输入台桌' : '请输入菜品'"
|
||||||
|
clearable @clear="inputClear"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="searchHandle">查询</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="right">
|
||||||
|
<el-form inline>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" style="width: 100px;">出菜</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
<div class="table_list_wrap" v-if="checkType == 1">
|
||||||
|
<div class="item list" :class="{ empty: tableList.length <= 0 }">
|
||||||
|
<div class="tab_item" v-for="item in tableList" :key="item.orderId" @click="changeTableItem(item)">
|
||||||
|
<div class="tab_header_wrap">
|
||||||
|
{{ item.tableName }} | {{ item.areaName }}
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="btn_wrap">
|
||||||
|
<div class="btn">
|
||||||
|
<el-button type="primary" style="width: 100%;">待出菜({{ item.pendingDishCount
|
||||||
|
}})</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="people_wrap">
|
||||||
|
<peopleIcon />
|
||||||
|
<span>1人</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="tableList.length <= 0"></el-empty>
|
||||||
|
</div>
|
||||||
|
<div class="item table_info">
|
||||||
|
<div class="table_info_wrap">
|
||||||
|
<el-button type="primary">{{ selectItem.tableName || '-' }} | {{ selectItem.areaName || '-'
|
||||||
|
}}</el-button>
|
||||||
|
<el-text>待出菜({{ selectItem.pendingDishCount || '-' }})</el-text>
|
||||||
|
<el-text>员工名称:{{ selectItem.staffName || notStaff }}</el-text>
|
||||||
|
<el-text>下单时间:{{ selectItem.orderTime || '-' }}</el-text>
|
||||||
|
</div>
|
||||||
|
<div class="table_wrap">
|
||||||
|
<el-table :data="tableData.list" border stripe>
|
||||||
|
<el-table-column label="菜品名称" prop="productName"></el-table-column>
|
||||||
|
<el-table-column label="用时" prop="startOrderTime">
|
||||||
|
<template v-slot="scope">
|
||||||
|
<el-text v-if="scope.row.subStatus == 'READY_TO_SERVE'">{{
|
||||||
|
formatTimeDiff(scope.row.startOrderTime) }}</el-text>
|
||||||
|
<el-text v-if="scope.row.subStatus == 'TIMEOUT'">{{ scope.row.timeout_time }}</el-text>
|
||||||
|
<el-text v-if="scope.row.subStatus == 'SENT_OUT'">{{ scope.row.dishOutTime }}</el-text>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" prop="subStatus" width="150">
|
||||||
|
<template v-slot="scope">
|
||||||
|
<el-tag disable-transitions :type="statusFilter(scope.row.subStatus).type" size="large">
|
||||||
|
{{ statusFilter(scope.row.subStatus).label }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="150">
|
||||||
|
<template v-slot="scope">
|
||||||
|
<el-button type="primary"
|
||||||
|
v-if="scope.row.subStatus == 'READY_TO_SERVE' || scope.row.subStatus == 'TIMEOUT'"
|
||||||
|
@click="upOrderDetailAjax(scope.row)">出菜</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="goods_list_wrap" v-else>
|
||||||
|
<div class="tablef_head">
|
||||||
|
<div class="item" @click="changeGoodsIndexHandle({ id: '' }, -1)">
|
||||||
|
<el-text :type="goodsListActive == -1 ? 'primary' : ''">全部</el-text>
|
||||||
|
</div>
|
||||||
|
<div class="item" v-for="(item, index) in categorys" :key="item.id"
|
||||||
|
@click="changeGoodsIndexHandle(item, index)">
|
||||||
|
<el-text :type="goodsListActive == index ? 'primary' : ''">{{ item.name }}</el-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table_wrap">
|
||||||
|
<div class="table" v-for="(item, index) in goodsTableData" :key="index">
|
||||||
|
<div class="goods_table_title">
|
||||||
|
<el-text size="large">{{ item.productName }}</el-text>
|
||||||
|
</div>
|
||||||
|
<el-table :data="item.foodItems" border stripe>
|
||||||
|
<el-table-column label="下单台桌" prop="areaName"></el-table-column>
|
||||||
|
<el-table-column label="用时" prop="startOrderTime">
|
||||||
|
<template v-slot="scope">
|
||||||
|
<el-text>{{ formatTimeDiff(scope.row.startOrderTime) }}</el-text>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" prop="subStatus" width="150">
|
||||||
|
<template v-slot="scope">
|
||||||
|
<el-tag disable-transitions :type="statusFilter(scope.row.subStatus).type" size="large">
|
||||||
|
{{ statusFilter(scope.row.subStatus).label }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="下单时间" prop="orderTime"></el-table-column>
|
||||||
|
<el-table-column label="员工" prop="staffName">
|
||||||
|
<template v-slot="scope">
|
||||||
|
{{ scope.row.staffName || notStaff }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="150">
|
||||||
|
<template v-slot="scope">
|
||||||
|
<el-button type="primary"
|
||||||
|
v-if="scope.row.subStatus == 'READY_TO_SERVE' || scope.row.subStatus == 'TIMEOUT'"
|
||||||
|
@click="upOrderDetailAjax(scope.row)">出菜</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="goodsTableData.length <= 0"></el-empty>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useUserStore } from '@/stores/user';
|
||||||
|
import scanIcon from '@/components/icons/scanIcon.vue';
|
||||||
|
import peopleIcon from '@/components/icons/peopleIcon.vue';
|
||||||
|
import tableStatus from '@/utils/tableStatusList.js'
|
||||||
|
import { formatTimeDiff, isMoreThanSpecifiedMinutes, calculateTimeoutDuration } from '@/utils'
|
||||||
|
import { categoryList } from "@/api/product.js";
|
||||||
|
import { getKitchenTable, getKitchenTableFoods, getKitchenFood, upOrderDetail } from '@/api/order.js'
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { ElMessage, ElNotification } from 'element-plus';
|
||||||
|
import { useScanListener } from '@/composables/useScanListener'
|
||||||
|
import { useSocketStore } from "@/stores/socket.js";
|
||||||
|
import { onUnmounted } from 'vue';
|
||||||
|
|
||||||
|
const socketStore = useSocketStore()
|
||||||
|
|
||||||
|
const unsub = socketStore.subscribe(msg => {
|
||||||
|
console.log('got', msg)
|
||||||
|
if (msg.type == 'bc') {
|
||||||
|
checkTypeHandle({ value: checkType.value })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const notStaff = ref('管理员')
|
||||||
|
|
||||||
|
// 扫码获取扫码内容
|
||||||
|
const { lastScan } = useScanListener({
|
||||||
|
onScan(code) {
|
||||||
|
let str = code.split(':')
|
||||||
|
handleScan(str[1])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 扫码提示
|
||||||
|
function showScanTipsHandle() {
|
||||||
|
ElNotification({
|
||||||
|
type: 'warning',
|
||||||
|
title: '注意',
|
||||||
|
message: '请使用扫码设备扫描菜品小票出菜'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkTypeList = ref([
|
||||||
|
{
|
||||||
|
label: '按台桌查看',
|
||||||
|
value: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '按菜品查看',
|
||||||
|
value: 2
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const checkType = ref(1)
|
||||||
|
|
||||||
|
// 清除搜索内容
|
||||||
|
function inputClear() {
|
||||||
|
goodsListActive.value = -1
|
||||||
|
checkTypeHandle({ value: checkType.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
function searchHandle() {
|
||||||
|
goodsListActive.value = -1
|
||||||
|
checkTypeHandle({ value: checkType.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换查看类型
|
||||||
|
async function checkTypeHandle(item) {
|
||||||
|
checkType.value = item.value
|
||||||
|
if (item.value == 1) {
|
||||||
|
await getKitchenTableAjax()
|
||||||
|
} else {
|
||||||
|
await getKitchenFoodAjax()
|
||||||
|
}
|
||||||
|
startUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const categorys = ref([])
|
||||||
|
const categoryId = ref('')
|
||||||
|
|
||||||
|
async function categoryListAjax() {
|
||||||
|
try {
|
||||||
|
const res = await categoryList()
|
||||||
|
categorys.value = res
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryForm = reactive({
|
||||||
|
tableName: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 按台桌查看
|
||||||
|
const tableList = ref([])
|
||||||
|
const selectItem = ref({})
|
||||||
|
async function getKitchenTableAjax() {
|
||||||
|
try {
|
||||||
|
const res = await getKitchenTable({
|
||||||
|
tableName: queryForm.tableName
|
||||||
|
})
|
||||||
|
tableList.value = res
|
||||||
|
if (res.length > 0) {
|
||||||
|
selectItem.value = res[0]
|
||||||
|
getKitchenTableFoodsAjax()
|
||||||
|
} else {
|
||||||
|
selectItem.value = {}
|
||||||
|
tableData.list = []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换台桌
|
||||||
|
function changeTableItem(item) {
|
||||||
|
selectItem.value = item
|
||||||
|
getKitchenTableFoodsAjax()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按台桌查看 商品内容
|
||||||
|
const tableData = reactive({
|
||||||
|
list: []
|
||||||
|
})
|
||||||
|
async function getKitchenTableFoodsAjax() {
|
||||||
|
try {
|
||||||
|
const res = await getKitchenTableFoods({
|
||||||
|
orderId: selectItem.value.orderId,
|
||||||
|
tableCode: selectItem.value.tableCode,
|
||||||
|
isNoTable: selectItem.value.tableCode ? '' : 1
|
||||||
|
})
|
||||||
|
|
||||||
|
res.forEach(item => {
|
||||||
|
item.timeout_time = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
tableData.list = res
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按菜品查看
|
||||||
|
const goodsListActive = ref(-1)
|
||||||
|
const goodsList = ref([])
|
||||||
|
const goodsTableData = ref([])
|
||||||
|
async function getKitchenFoodAjax() {
|
||||||
|
try {
|
||||||
|
const res = await getKitchenFood({
|
||||||
|
productName: queryForm.tableName,
|
||||||
|
categoryId: categoryId.value
|
||||||
|
})
|
||||||
|
goodsList.value = res
|
||||||
|
goodsTableData.value = goodsList.value
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换类型
|
||||||
|
function changeGoodsIndexHandle(item, index) {
|
||||||
|
categoryId.value = item.id
|
||||||
|
goodsListActive.value = index
|
||||||
|
// if (index == -1) {
|
||||||
|
// goodsTableData.value = goodsList.value
|
||||||
|
// } else {
|
||||||
|
// goodsTableData.value = [goodsList.value[index]]
|
||||||
|
// }
|
||||||
|
getKitchenFoodAjax()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扫码出菜
|
||||||
|
function handleScan(code) {
|
||||||
|
console.log('handleScan', code);
|
||||||
|
try {
|
||||||
|
upOrderDetailAjax({
|
||||||
|
orderDetailId: code
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('handleScan error', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 出菜
|
||||||
|
async function upOrderDetailAjax(item) {
|
||||||
|
try {
|
||||||
|
await upOrderDetail({
|
||||||
|
orderDetailId: item.orderDetailId,
|
||||||
|
type: 2
|
||||||
|
})
|
||||||
|
ElMessage.success('出菜成功');
|
||||||
|
checkTypeHandle({ value: checkType.value })
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜品状态 待起菜 PENDING_PREP 待出菜 READY_TO_SERVE 已出菜 SENT_OUT 已上菜 DELIVERED
|
||||||
|
function statusFilter(status) {
|
||||||
|
let statusList = [
|
||||||
|
{
|
||||||
|
value: 'PENDING_PREP',
|
||||||
|
label: '待起菜',
|
||||||
|
type: 'info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'READY_TO_SERVE',
|
||||||
|
label: '待出菜',
|
||||||
|
type: 'warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'SENT_OUT',
|
||||||
|
label: '已出菜',
|
||||||
|
type: 'success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'DELIVERED',
|
||||||
|
label: '已上菜',
|
||||||
|
type: 'primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'TIMEOUT',
|
||||||
|
label: '已超时',
|
||||||
|
type: 'danger'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
let obj = statusList.find(item => item.value == status)
|
||||||
|
if (obj && obj.value) {
|
||||||
|
return obj
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
value: status,
|
||||||
|
label: '未知',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新所有数据的状态
|
||||||
|
function updateOrderStatus() {
|
||||||
|
console.log('刷新所有数据的状态.tableData.list', tableData.list);
|
||||||
|
console.log('刷新所有数据的状态.goodsTableData.value', goodsTableData.value);
|
||||||
|
|
||||||
|
if (checkType.value == 1) {
|
||||||
|
// 刷新按台桌查看的数据
|
||||||
|
tableData.list.forEach(item => {
|
||||||
|
let timeSpent = formatTimeDiff(item.startOrderTime)
|
||||||
|
if (item.subStatus == 'READY_TO_SERVE' && isMoreThanSpecifiedMinutes(timeSpent, userStore.shopInfo.serveTime)) {
|
||||||
|
item.subStatus = 'TIMEOUT'
|
||||||
|
item.timeout_time = calculateTimeoutDuration(timeSpent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 刷新按菜品查看的数据
|
||||||
|
goodsTableData.value.forEach(val => {
|
||||||
|
val.foodItems.forEach(item => {
|
||||||
|
let timeSpent = formatTimeDiff(item.startOrderTime)
|
||||||
|
if (item.subStatus == 'READY_TO_SERVE' && isMoreThanSpecifiedMinutes(timeSpent, userStore.shopInfo.serveTime)) {
|
||||||
|
item.subStatus = 'TIMEOUT'
|
||||||
|
item.timeout_time = calculateTimeoutDuration(timeSpent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动刷新
|
||||||
|
const timer = ref(null)
|
||||||
|
function startUpdate() {
|
||||||
|
if (userStore.shopInfo.isServeTimeControl && (tableList.length || tableData.list.length)) {
|
||||||
|
// 只有开启的情况下调用
|
||||||
|
updateOrderStatus()
|
||||||
|
if (timer.value !== null) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
}
|
||||||
|
timer.value = setInterval(() => {
|
||||||
|
updateOrderStatus()
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
checkTypeHandle({ value: checkType.value })
|
||||||
|
categoryListAjax()
|
||||||
|
socketStore.init(import.meta.env.VITE_API_WSS)
|
||||||
|
socketStore.startHeartbeat()
|
||||||
|
|
||||||
|
// 如果需要在打开时发送初始化,可使用 onOpen 注册一次性回调:
|
||||||
|
const off = socketStore.onOpen(() => {
|
||||||
|
socketStore.send({ operate_type: 'init' })
|
||||||
|
off() // 若只需一次,注册后立即取消
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
this.timer = null
|
||||||
|
unsub.close()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.container {
|
||||||
|
--padding: 24px;
|
||||||
|
|
||||||
|
.header_wrap {
|
||||||
|
--height: 57px;
|
||||||
|
display: flex;
|
||||||
|
height: var(--height);
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 var(--padding);
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: var(--height);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
--size: 8px;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
background-color: var(--el-color-danger);
|
||||||
|
|
||||||
|
&.online {
|
||||||
|
background-color: var(--el-color-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab_wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid #ececec;
|
||||||
|
padding: 0 var(--padding) 14px;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_wrap {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 16px var(--padding) 0 var(--padding);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_list_wrap {
|
||||||
|
margin-top: 14px;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: var(--padding);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--padding);
|
||||||
|
|
||||||
|
.item {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&.list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
grid-template-rows: repeat(auto, 1fr);
|
||||||
|
grid-column-gap: var(--padding);
|
||||||
|
grid-row-gap: var(--padding);
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab_item {
|
||||||
|
height: 150px;
|
||||||
|
background-color: #3F9EFF;
|
||||||
|
padding: 7px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
transform: translateY(-10px) scale(1.1);
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab_header_wrap {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0 7px;
|
||||||
|
|
||||||
|
.btn_wrap {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.people_wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 10px 0 20px 0;
|
||||||
|
border-top: 1px solid #ececec;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.table_info {
|
||||||
|
.table_info_wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_wrap {
|
||||||
|
padding-top: var(--padding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods_list_wrap {
|
||||||
|
margin-top: 14px;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: var(--padding);
|
||||||
|
gap: var(--padding);
|
||||||
|
|
||||||
|
.tablef_head {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods_table_title {
|
||||||
|
padding: 10px 0 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
311
src/renderer/src/views/login.vue
Normal file
311
src/renderer/src/views/login.vue
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
<!-- 登录 -->
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="login_wrap">
|
||||||
|
<div class="logo_wrap">
|
||||||
|
<img src="@/assets/logo_text.png" alt="logo" />
|
||||||
|
<span>银收客订餐</span>
|
||||||
|
</div>
|
||||||
|
<div class="login_content">
|
||||||
|
<div class="lgoin_tab">
|
||||||
|
<div class="item" :class="{ active: form.loginType == 0 }" @click="form.loginType = 0">
|
||||||
|
<div class="t">商户登录</div>
|
||||||
|
</div>
|
||||||
|
<div class="item" :class="{ active: form.loginType == 1 }" @click="form.loginType = 1">
|
||||||
|
<div class="t">员工登录</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form_wrap" :class="[`radius${form.loginType}`]">
|
||||||
|
<div class="title_wrap">
|
||||||
|
<span class="t1">欢迎登录</span>
|
||||||
|
<span class="t2">welcome to login in</span>
|
||||||
|
</div>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules">
|
||||||
|
<el-form-item prop="username">
|
||||||
|
<el-input v-model="form.username" clearable prefix-icon="User" size="large" :maxlength="20"
|
||||||
|
placeholder="请输入用户名/手机号"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="staffUserName" v-if="form.loginType == 1">
|
||||||
|
<el-input v-model="form.staffUserName" clearable prefix-icon="User" size="large"
|
||||||
|
:maxlength="20" placeholder="请输入员工用户名/手机号"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input v-model="form.password" clearable prefix-icon="Lock" size="large" :maxlength="20"
|
||||||
|
placeholder="请输入登录密码"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="code">
|
||||||
|
<div class="ipt_wrap">
|
||||||
|
<div class="ipt">
|
||||||
|
<el-input v-model="form.code" clearable prefix-icon="Warning" size="large"
|
||||||
|
:maxlength="6" placeholder="请输入验证码" style="width: 100%;"></el-input>
|
||||||
|
</div>
|
||||||
|
<div class="code" @click="captchaAjax">
|
||||||
|
<img class="img" :src="codeImgUrl">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div class="btn_wrap">
|
||||||
|
<el-button type="primary" size="large" style="width: 100%;" :loading="loading"
|
||||||
|
@click="submitHandle">登录</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { captcha } from '@/api/account.js'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const formRef = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const form = ref({
|
||||||
|
loginType: 0, // 登录类型 0:商户登录 1:员工登录
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
staffUserName: '',
|
||||||
|
code: '',
|
||||||
|
uuid: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const codeImgUrl = ref('')
|
||||||
|
|
||||||
|
const rules = ref({
|
||||||
|
username: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: ' ',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: ' ',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
staffUserName: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: ' ',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
code: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: ' ',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
function submitHandle() {
|
||||||
|
formRef.value.validate(vaild => {
|
||||||
|
if (vaild) {
|
||||||
|
loading.value = true
|
||||||
|
userStore.login(form.value).then(res => {
|
||||||
|
router.replace({ name: 'home' })
|
||||||
|
}).catch(err => {
|
||||||
|
loading.value = false
|
||||||
|
console.log(err);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取验证码
|
||||||
|
async function captchaAjax() {
|
||||||
|
try {
|
||||||
|
const { code, uuid } = await captcha()
|
||||||
|
form.value.uuid = uuid
|
||||||
|
codeImgUrl.value = code
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
captchaAjax()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: url('@/assets/logo_bg.png') no-repeat center center / cover;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.login_wrap {
|
||||||
|
width: 420px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 4%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
|
||||||
|
.logo_wrap {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 93px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 40px;
|
||||||
|
color: #3D66B0;
|
||||||
|
font-weight: bold;
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login_content {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #F3F4F5;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.lgoin_tab {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
background-color: #F3F4F5;
|
||||||
|
padding-top: 10px;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 50px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.t {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.t {
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
width: 110%;
|
||||||
|
height: 110%;
|
||||||
|
background-color: #fff;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
width: 40px;
|
||||||
|
height: 2px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 6px;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 2;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
&.active {
|
||||||
|
&::after {
|
||||||
|
border-radius: 0 20px 0 0;
|
||||||
|
transform: skewX(20deg) translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
&.active {
|
||||||
|
&::after {
|
||||||
|
border-radius: 20px 0 0 0;
|
||||||
|
transform: skewX(-20deg) translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form_wrap {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 24px 37px;
|
||||||
|
|
||||||
|
&.radius0 {
|
||||||
|
border-radius: 0 20px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.radius1 {
|
||||||
|
border-radius: 20px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title_wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
|
||||||
|
.t1 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t2 {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ipt_wrap {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.ipt {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
width: 100px;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
.img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn_wrap {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
src/renderer/vite.config.js
Normal file
14
src/renderer/vite.config.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
// 本地 vite 配置,用于在 src/renderer 目录直接运行 `vite build` 或 `vite` 时
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
// 将 '@' 映射到 src/renderer/src,保持与主工程的别名一致
|
||||||
|
'@': resolve(__dirname, 'src')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user