呼号端初版
parent
5a11975bd5
commit
80d82aaee4
@ -1,22 +1,22 @@
|
||||
{
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"connectionTimeout": 15,
|
||||
"mssqlOptions": {
|
||||
"appName": "SQLTools",
|
||||
"useUTC": true,
|
||||
"encrypt": true,
|
||||
"trustServerCertificate": false
|
||||
},
|
||||
"ssh": "Disabled",
|
||||
"previewLimit": 50,
|
||||
"server": "47.96.78.103",
|
||||
"port": 1433,
|
||||
"askForPassword": true,
|
||||
"driver": "MSSQL",
|
||||
"name": "QS aliyun",
|
||||
"username": "sa",
|
||||
"database": "QueuingSystem"
|
||||
}
|
||||
]
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"connectionTimeout": 15,
|
||||
"mssqlOptions": {
|
||||
"appName": "SQLTools",
|
||||
"useUTC": true,
|
||||
"encrypt": true,
|
||||
"trustServerCertificate": false
|
||||
},
|
||||
"ssh": "Disabled",
|
||||
"previewLimit": 50,
|
||||
"server": "47.96.78.103",
|
||||
"port": 1433,
|
||||
"askForPassword": true,
|
||||
"driver": "MSSQL",
|
||||
"name": "QS aliyun",
|
||||
"username": "sa",
|
||||
"database": "QueuingSystem"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
.gitignore
|
||||
@ -1,23 +0,0 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'@electron-toolkit',
|
||||
'@electron-toolkit/eslint-config-ts/eslint-recommended',
|
||||
'@vue/eslint-config-typescript/recommended',
|
||||
'@vue/eslint-config-prettier'
|
||||
],
|
||||
rules: {
|
||||
'vue/require-default-prop': 'off',
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'prefer-const': 'false'
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
singleQuote: true
|
||||
semi: false
|
||||
printWidth: 100
|
||||
trailingComma: none
|
||||
tabWidth: 2
|
||||
endOfLine: auto
|
||||
trailingComma: es5
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 611 KiB |
@ -1,12 +0,0 @@
|
||||
<?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>
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 121 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB |
@ -0,0 +1,3 @@
|
||||
provider: generic
|
||||
url: https://example.com/auto-updates
|
||||
updaterCacheDirName: call-client-updater
|
||||
@ -1,20 +1,16 @@
|
||||
import { resolve } from 'path'
|
||||
import { defineConfig, externalizeDepsPlugin, bytecodePlugin } from 'electron-vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'electron-vite'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [externalizeDepsPlugin(), bytecodePlugin()]
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin(), bytecodePlugin()]
|
||||
},
|
||||
main: {},
|
||||
preload: {},
|
||||
renderer: {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@renderer': resolve('src/renderer/src')
|
||||
}
|
||||
'@renderer': resolve('src/renderer/src'),
|
||||
},
|
||||
},
|
||||
plugins: [vue()]
|
||||
}
|
||||
plugins: [vue()],
|
||||
},
|
||||
})
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
import { defineConfig } from 'eslint/config'
|
||||
import tseslint from '@electron-toolkit/eslint-config-ts'
|
||||
import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier'
|
||||
import eslintPluginVue from 'eslint-plugin-vue'
|
||||
import vueParser from 'vue-eslint-parser'
|
||||
|
||||
export default defineConfig(
|
||||
{ ignores: ['**/node_modules', '**/dist', '**/out'] },
|
||||
tseslint.configs.recommended,
|
||||
eslintPluginVue.configs['flat/recommended'],
|
||||
{
|
||||
files: ['**/*.vue'],
|
||||
languageOptions: {
|
||||
parser: vueParser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
extraFileExtensions: ['.vue'],
|
||||
parser: tseslint.parser,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
rules: {
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'vue/no-v-for-template-key': 'off',
|
||||
'vue/block-lang': [
|
||||
'error',
|
||||
{
|
||||
script: {
|
||||
lang: 'ts',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
eslintConfigPrettier,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,8 +1 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI
|
||||
api: unknown
|
||||
}
|
||||
}
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
import type { CallRequest, CallResponse, PauseRequest, ReCallRequest } from '@renderer/types/action'
|
||||
import type { UserRequest, UserResponse } from '@renderer/types/user'
|
||||
import type { WindowResponse } from '@renderer/types/window'
|
||||
import { http } from '@renderer/utils/service'
|
||||
|
||||
export const api = {
|
||||
// 用户相关
|
||||
user: {
|
||||
login: (data: UserRequest) => http.post<UserResponse>('/auth/login', data),
|
||||
},
|
||||
|
||||
// 窗口
|
||||
window: {
|
||||
list: () => http.get<WindowResponse>('/windows/list'),
|
||||
},
|
||||
|
||||
action: {
|
||||
call: (data: CallRequest) => http.post<CallResponse>('/call-terminal/call', data),
|
||||
recall: (data: ReCallRequest) => http.post<CallResponse>('/call-terminal/recall', data),
|
||||
abandon: (data: ReCallRequest) => http.post<CallResponse>('/call-terminal/abandon', data),
|
||||
pause: (data: PauseRequest) => http.post<CallResponse>('/call-terminal/pause', data),
|
||||
resume: (data: CallRequest) => http.post<CallResponse>('/call-terminal/resume', data),
|
||||
start: (data: ReCallRequest) => http.post<CallResponse>('/call-terminal/start', data),
|
||||
complete: (data: ReCallRequest) => http.post<CallResponse>('/call-terminal/complete', data),
|
||||
evaluate: (data: ReCallRequest) => http.post<CallResponse>('/call-terminal/evaluate', data),
|
||||
},
|
||||
}
|
||||
@ -1,8 +1,41 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
export {}
|
||||
|
||||
interface IWinControl {
|
||||
windowMinimize: () => void
|
||||
windowClose: () => void
|
||||
loginSuccess: () => void
|
||||
}
|
||||
|
||||
interface IContextMenu {
|
||||
showContextMenu: () => void
|
||||
}
|
||||
|
||||
interface IPauseMenu {
|
||||
showPauseMenu: () => void
|
||||
pauseReasonSelected: (callback: (reason: string) => void) => void
|
||||
}
|
||||
|
||||
interface IStore {
|
||||
get: (key: string) => unknown
|
||||
set: (key: string, val: unknown) => void
|
||||
delete: (key: string) => void
|
||||
has: (key: string) => boolean
|
||||
}
|
||||
|
||||
interface ISession {
|
||||
get: () => SessionState
|
||||
set: (SessionState) => boolean
|
||||
clear: () => boolean
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
winControl: IWinControl
|
||||
contextMenu: IContextMenu
|
||||
store: IStore
|
||||
session: ISession
|
||||
pauseMenu: IPauseMenu
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './assets/main.css'
|
||||
import router from './router'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
createApp(App).use(router).use(ElementPlus).mount('#app')
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import Login from '@renderer/views/Login.vue'
|
||||
import Main from '@renderer/views/Main.vue'
|
||||
import TicketList from '@renderer/views/TicketList.vue'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
export default createRouter({
|
||||
history: createWebHashHistory(), // hash模式
|
||||
routes: [
|
||||
{ path: '/', component: Login },
|
||||
{ path: '/login', component: Login },
|
||||
{ path: '/main', component: Main }]
|
||||
{ path: '/main', component: Main },
|
||||
{ path: '/ticketList', component: TicketList },
|
||||
],
|
||||
})
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
export type CallStatus = 'idle' | 'calling' | 'paused' | 'working' | 'evaluating' | 'transferring'
|
||||
|
||||
export interface ActionButton {
|
||||
icon: unknown
|
||||
label: string
|
||||
action: string
|
||||
}
|
||||
|
||||
// 呼叫
|
||||
export interface CallRequest {
|
||||
windowUid: number
|
||||
empUid: number
|
||||
}
|
||||
|
||||
export interface CallResponse {
|
||||
action: string
|
||||
success: boolean
|
||||
message: string
|
||||
windowUid: number
|
||||
windowName: string
|
||||
ticketNo: string
|
||||
ticketStatus: number
|
||||
ticketStatusText: string
|
||||
ticketUid: number
|
||||
}
|
||||
|
||||
// 重呼
|
||||
export interface ReCallRequest {
|
||||
windowUid: number
|
||||
empUid: number
|
||||
ticketUid: number
|
||||
}
|
||||
|
||||
// 暂停
|
||||
export interface PauseRequest {
|
||||
windowUid: number
|
||||
empUid: number
|
||||
pauseReason: string
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
// types/http.ts
|
||||
export interface ApiResponse<T = unknown> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
export interface UserRequest {
|
||||
loginMode: string
|
||||
clientType: string
|
||||
username: string
|
||||
password: string
|
||||
hallRegNum: string
|
||||
// tenantId: string
|
||||
}
|
||||
|
||||
export interface UserResponse {
|
||||
queueToken: string
|
||||
refreshToken: string
|
||||
tokenType: string
|
||||
expiresIn: string
|
||||
operatorProfile: UserInfo
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
empUid: number
|
||||
name: string
|
||||
username: string
|
||||
avatar: string
|
||||
tenantId: string
|
||||
hallCode: string
|
||||
hallName: string
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
export interface WindowResponse {
|
||||
windows: Window[]
|
||||
}
|
||||
|
||||
export interface Window {
|
||||
windowUid: number
|
||||
windowCode: string
|
||||
windowName: string
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export interface SessionState {
|
||||
empUid: number | null
|
||||
winUid: number | null
|
||||
queueToken: string | null
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }]
|
||||
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }],
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,8 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@types/cacheable-request": "^6.0.3",
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/http-cache-semantics": "^4.2.0",
|
||||
"@types/keyv": "^3.1.4"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import request from '@/utils/request.js'
|
||||
|
||||
// 获取首页概览数据
|
||||
export const getOverview = (params = {}) => {
|
||||
return request({
|
||||
tag: 'pad.hallSystem',
|
||||
path: '/overview/metrics',
|
||||
method: 'GET',
|
||||
query: params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取大厅system表数据
|
||||
export const getSystemList = (params = {}) => {
|
||||
return request({
|
||||
tag: 'pad.hallSystem',
|
||||
path: '/list',
|
||||
method: 'GET',
|
||||
query: params
|
||||
})
|
||||
}
|
||||
|
||||
// 修改大厅system表数据
|
||||
export const updateSysValue = (params = {}) => {
|
||||
return request({
|
||||
tag: 'pad.hallSystem',
|
||||
path: '/value',
|
||||
method: 'POST',
|
||||
query: params
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import request from '@/utils/request.js'
|
||||
|
||||
// 取窗口号
|
||||
export const takeTicket = (params = {}) => {
|
||||
return request({
|
||||
tag: 'pad.ticket',
|
||||
path: '/take',
|
||||
method: 'POST',
|
||||
body: params
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import request from '@/utils/request.js'
|
||||
|
||||
// 获取首页概览数据
|
||||
export const getMonitorList = (params = {}) => {
|
||||
return request({
|
||||
tag: 'pad.window',
|
||||
path: '/monitor/list',
|
||||
method: 'GET',
|
||||
query: params
|
||||
})
|
||||
}
|
||||
Binary file not shown.
@ -1,19 +1,46 @@
|
||||
import { app as e, BrowserWindow as r } from "electron";
|
||||
import { fileURLToPath as t } from "url";
|
||||
import o from "path";
|
||||
const a = t(import.meta.url), i = o.dirname(a);
|
||||
e.whenReady().then(() => {
|
||||
const n = new r({
|
||||
width: 800,
|
||||
height: 600,
|
||||
import { app, BrowserWindow, ipcMain } from "electron";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
const __filename$1 = fileURLToPath(import.meta.url);
|
||||
const __dirname$1 = path.dirname(__filename$1);
|
||||
let mainWindow = null;
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
const createWindow = () => {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: !1,
|
||||
contextIsolation: !0,
|
||||
preload: o.join(i, "../preload/index.js")
|
||||
}
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
// preload: path.join(__dirname, '../../../dist-electron/preload/index.js'),
|
||||
},
|
||||
autoHideMenuBar: true
|
||||
});
|
||||
process.env.VITE_DEV_SERVER_URL ? n.loadURL(process.env.VITE_DEV_SERVER_URL) : n.loadFile(o.join(i, "../../dist/index.html"));
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname$1, "../../dist/index.html"));
|
||||
}
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
};
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
app.on("activate", function() {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
});
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
e.on("window-all-closed", () => {
|
||||
process.platform !== "darwin" && e.quit();
|
||||
ipcMain.handle("print-ticket", async (event, ticketInfo) => {
|
||||
console.log("打印小票:", ticketInfo, event);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
||||
return {
|
||||
success: true,
|
||||
message: "小票打印成功"
|
||||
};
|
||||
});
|
||||
|
||||
@ -1,5 +1,15 @@
|
||||
import { contextBridge as o } from "electron";
|
||||
o.exposeInMainWorld("electronAPI", {
|
||||
// 可以在这里添加需要暴露的API
|
||||
platform: process.platform
|
||||
import { contextBridge, ipcRenderer } from "electron";
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
// 打印相关
|
||||
printTicket: (ticketInfo) => ipcRenderer.invoke("print-ticket", ticketInfo),
|
||||
// 业务相关
|
||||
getBusinesses: () => ipcRenderer.invoke("get-businesses"),
|
||||
// 窗口控制
|
||||
minimize: () => ipcRenderer.send("window-minimize"),
|
||||
maximize: () => ipcRenderer.send("window-maximize"),
|
||||
close: () => ipcRenderer.send("window-close"),
|
||||
// 监听事件
|
||||
onWindowStateChange: (callback) => {
|
||||
ipcRenderer.on("window-state-changed", (_, state) => callback(state));
|
||||
}
|
||||
});
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
const { build } = require('vite-plugin-electron')
|
||||
|
||||
module.exports = {
|
||||
productName: 'Ticket Client',
|
||||
copyright: 'Copyright © 2026',
|
||||
files: ['dist', 'dist-electron', '!**/package-lock.json'],
|
||||
directories: {
|
||||
output: 'release/${version}',
|
||||
buildResources: 'build-resources',
|
||||
},
|
||||
asar: true,
|
||||
compression: 'maximum',
|
||||
|
||||
win: {
|
||||
icon: 'build-resources/icon.ico',
|
||||
target: [
|
||||
{
|
||||
target: 'nsis',
|
||||
arch: ['x64'],
|
||||
},
|
||||
],
|
||||
},
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
allowElevation: true,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
createDesktopShortcut: true,
|
||||
createStartMenuShortcut: true,
|
||||
shortcutName: 'Ticket Client',
|
||||
uninstallDisplayName: 'Ticket Client',
|
||||
runAfterFinish: false,
|
||||
include: 'build/installer.nsh',
|
||||
perMachine: true,
|
||||
deleteAppDataOnUninstall: true,
|
||||
},
|
||||
}
|
||||
@ -1,30 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src="/vite.svg" class="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://vuejs.org/" target="_blank">
|
||||
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
|
||||
</a>
|
||||
</div>
|
||||
<HelloWorld msg="Vite + Vue" />
|
||||
<HomeView />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
<script setup lang="ts">
|
||||
import HomeView from '../src/views/HomeView.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
|
||||
body {
|
||||
font-family:
|
||||
'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑',
|
||||
Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<header class="app-header">
|
||||
<div class="header-left">
|
||||
<h1 class="app-title">
|
||||
<el-icon class="title-icon"><OfficeBuilding /></el-icon>
|
||||
业务办理系统
|
||||
</h1>
|
||||
<div class="header-subtitle">欢迎使用自助服务终端</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="header-info">
|
||||
<div class="current-time">
|
||||
<el-icon><Clock /></el-icon>
|
||||
<span>{{ currentTime }}</span>
|
||||
</div>
|
||||
<div class="server-status">
|
||||
<!-- <el-tag :type="isOnline ? 'success' : 'danger'" size="small">
|
||||
<el-icon :class="isOnline ? 'online-icon' : 'offline-icon'">
|
||||
{{ isOnline ? 'SuccessFilled' : 'WarningFilled' }}
|
||||
</el-icon>
|
||||
{{ isOnline ? '在线' : '离线' }}
|
||||
</el-tag> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<el-tooltip content="刷新业务列表" placement="bottom">
|
||||
<el-button type="info" circle @click="handleRefresh" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- <el-tooltip content="系统设置" placement="bottom">
|
||||
<el-button type="info" circle>
|
||||
<el-icon><Setting /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip> -->
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Clock, OfficeBuilding, Refresh } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'refresh'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const currentTime = ref('')
|
||||
const isOnline = ref(true)
|
||||
|
||||
const updateCurrentTime = () => {
|
||||
const now = new Date()
|
||||
currentTime.value = now.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
emit('refresh')
|
||||
ElMessage.success('刷新成功')
|
||||
}
|
||||
|
||||
const checkOnlineStatus = async () => {
|
||||
try {
|
||||
// const response = await fetch('https://www.google.com', { mode: 'no-cors' })
|
||||
isOnline.value = true
|
||||
} catch {
|
||||
isOnline.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateCurrentTime()
|
||||
const timer = setInterval(updateCurrentTime, 1000)
|
||||
|
||||
// 每30秒检查一次网络状态
|
||||
checkOnlineStatus()
|
||||
const networkTimer = setInterval(checkOnlineStatus, 30000)
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
clearInterval(networkTimer)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AppHeader',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-header {
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #1a1a2e 100%);
|
||||
padding: 16px 24px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: white;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.app-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.title-icon {
|
||||
font-size: 28px;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-left: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.el-button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.current-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
|
||||
.el-icon {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.server-status {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.el-tag {
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
.el-icon {
|
||||
margin-right: 4px;
|
||||
|
||||
&.online-icon {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.offline-icon {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<div
|
||||
class="business-card"
|
||||
:style="{ '--business-color': business.color || '#409EFF' }"
|
||||
@mouseenter="isHovered = true"
|
||||
@mouseleave="isHovered = false"
|
||||
>
|
||||
<el-button
|
||||
class="business-button"
|
||||
:type="buttonType"
|
||||
@click="handleClick"
|
||||
:loading="isProcessing"
|
||||
:disabled="isProcessing || disabled"
|
||||
:circle="isCompact"
|
||||
>
|
||||
<div class="business-content" :class="{ compact: isCompact }">
|
||||
<!-- <div class="business-icon-wrapper">
|
||||
<div class="business-icon">
|
||||
<el-icon v-if="business.icon" :size="iconSize">
|
||||
<component :is="business.icon" />
|
||||
</el-icon>
|
||||
<el-icon v-else :size="iconSize"><Operation /></el-icon>
|
||||
</div>
|
||||
|
||||
<div v-if="showBadge" class="business-badge" :class="badgeType">
|
||||
{{ badgeText }}
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="business-icon-wrapper">
|
||||
<span class="business-prefix">{{ business.prefix }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!isCompact" class="business-info">
|
||||
<h3 class="business-name">{{ business.name }}</h3>
|
||||
<!-- <p v-if="business.description" class="business-desc">
|
||||
{{ business.description }}
|
||||
</p> -->
|
||||
<div class="business-footer">
|
||||
<!-- <span class="business-code">{{ business.prefix }}</span> -->
|
||||
<span class="business-hint">点击办理</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isCompact" class="business-compact-info">
|
||||
<span class="business-name">{{ business.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-button>
|
||||
|
||||
<transition name="slide-up">
|
||||
<div v-if="isHovered && !isCompact" class="business-hover-effect">
|
||||
<div class="ripple-effect" :style="rippleStyle"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { BusinessItem } from '../types/business'
|
||||
|
||||
interface Props {
|
||||
business: BusinessItem
|
||||
isProcessing?: boolean
|
||||
disabled?: boolean
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'click', business: BusinessItem): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const isHovered = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const buttonType = computed(() => {
|
||||
const colors: Record<string, string> = {
|
||||
'#409EFF': 'primary',
|
||||
'#67C23A': 'success',
|
||||
'#E6A23C': 'warning',
|
||||
'#F56C6C': 'danger',
|
||||
'#909399': 'info',
|
||||
}
|
||||
return colors[props.business.color || '#409EFF'] || 'primary'
|
||||
})
|
||||
|
||||
const isCompact = computed(() => props.compact || false)
|
||||
// const iconSize = computed(() => (isCompact.value ? 24 : 36))
|
||||
|
||||
// const showBadge = computed(() => props.business.id === 'hot')
|
||||
// const badgeType = computed(() => {
|
||||
// if (props.business.id === 'hot') return 'hot'
|
||||
// return ''
|
||||
// })
|
||||
// const badgeText = computed(() => {
|
||||
// if (props.business.id === 'hot') return '热门'
|
||||
// return ''
|
||||
// })
|
||||
|
||||
const rippleStyle = computed(() => ({
|
||||
backgroundColor: props.business.color || '#409EFF',
|
||||
opacity: isHovered.value ? 0.1 : 0,
|
||||
}))
|
||||
|
||||
// 方法
|
||||
const handleClick = () => {
|
||||
if (props.disabled || props.isProcessing) return
|
||||
|
||||
ElMessage.info(`开始办理 ${props.business.name}`)
|
||||
emit('click', props.business)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'BusinessCard',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.business-card {
|
||||
--business-color: #409eff;
|
||||
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.business-button {
|
||||
width: 100%;
|
||||
height: 120px; /* 修改这里设置固定高度 */
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 16px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: flex-start; /* 内容靠左显示 */
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 12px 30px rgba(var(--business-color, 64, 158, 255), 0.25);
|
||||
border-color: var(--business-color);
|
||||
|
||||
.business-icon-wrapper {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.is-circle {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.business-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-align: left;
|
||||
height: 100%;
|
||||
|
||||
&.compact {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.business-icon-wrapper {
|
||||
position: relative;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
.business-prefix {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(var(--business-color, 64, 158, 255), 0.15) 0%,
|
||||
rgba(var(--business-color, 64, 158, 255), 0.05) 100%
|
||||
);
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(var(--business-color, 64, 158, 255), 0.3);
|
||||
color: var(--business-color);
|
||||
}
|
||||
|
||||
.business-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(var(--business-color, 64, 158, 255), 0.15) 0%,
|
||||
rgba(var(--business-color, 64, 158, 255), 0.05) 100%
|
||||
);
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(var(--business-color, 64, 158, 255), 0.3);
|
||||
|
||||
.el-icon {
|
||||
color: var(--business-color);
|
||||
}
|
||||
}
|
||||
|
||||
.business-badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
|
||||
&.hot {
|
||||
background: linear-gradient(45deg, #ff6b6b, #ffa726);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.business-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.business-name {
|
||||
margin: 0 0 8px 0;
|
||||
color: #303133;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.business-desc {
|
||||
margin: 0 0 12px 0;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.business-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.business-code {
|
||||
padding: 4px 10px;
|
||||
background: rgba(var(--business-color, 64, 158, 255), 0.08);
|
||||
color: var(--business-color);
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.business-hint {
|
||||
color: #c0c4cc;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.business-compact-info {
|
||||
.business-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.business-hover-effect {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 8px;
|
||||
pointer-events: none;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
.ripple-effect {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 动画
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-up-enter-from,
|
||||
.slide-up-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="business-grid">
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-skeleton :rows="6" animated />
|
||||
</div>
|
||||
|
||||
<div v-else class="grid-container">
|
||||
<BusinessCard
|
||||
v-for="business in businesses"
|
||||
:key="business.uid"
|
||||
:business="business"
|
||||
:is-processing="isProcessing && processingBusinessId === business.tenantId"
|
||||
:disabled="isProcessing && processingBusinessId !== business.tenantId"
|
||||
@click="handleBusinessClick(business)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && businesses.length === 0" class="empty-state">
|
||||
<el-empty description="暂无业务数据" />
|
||||
<el-button type="primary" @click="$emit('refresh')">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { BusinessItem } from '../types/business'
|
||||
import BusinessCard from './BusinessCard.vue'
|
||||
|
||||
interface Props {
|
||||
businesses: BusinessItem[]
|
||||
loading: boolean
|
||||
isProcessing?: boolean
|
||||
processingBusinessId?: string | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'business-click', business: BusinessItem): void
|
||||
(e: 'refresh'): void
|
||||
(e: 'retry'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 方法
|
||||
const handleBusinessClick = (business: BusinessItem) => {
|
||||
if (props.isProcessing) {
|
||||
ElMessage.warning('正在处理其他业务,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.info(`开始办理 ${business.name}`)
|
||||
emit('business-click', business)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'BusinessGrid',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.business-grid {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.loading-container {
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-auto-rows: 120px; /* 控制每行高度 */
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
|
||||
/* 隐藏滚动条但保持滚动功能 */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 20px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,41 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
@ -1,30 +1,59 @@
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { app, BrowserWindow, ipcMain } from 'electron'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
const createWindow = () => {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
// preload: path.join(__dirname, '../../../dist-electron/preload/index.js'),
|
||||
},
|
||||
autoHideMenuBar: true,
|
||||
})
|
||||
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL as string)
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, '../../dist/index.html'))
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools()
|
||||
}
|
||||
}
|
||||
app.whenReady().then(() => {
|
||||
const win = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, '../preload/index.js')
|
||||
}
|
||||
})
|
||||
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
win.loadURL(process.env.VITE_DEV_SERVER_URL as string)
|
||||
} else {
|
||||
win.loadFile(path.join(__dirname, '../../dist/index.html'))
|
||||
}
|
||||
createWindow()
|
||||
|
||||
app.on('activate', function () {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
// IPC处理器
|
||||
ipcMain.handle('print-ticket', async (event, ticketInfo) => {
|
||||
// 这里可以连接实际的打印机
|
||||
console.log('打印小票:', ticketInfo, event)
|
||||
|
||||
// 模拟打印延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '小票打印成功',
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,8 +1,34 @@
|
||||
// 预加载脚本
|
||||
import { contextBridge } from 'electron'
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
|
||||
// 暴露API给渲染进程
|
||||
// 暴露安全的API给渲染进程
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// 可以在这里添加需要暴露的API
|
||||
platform: process.platform
|
||||
// 打印相关
|
||||
printTicket: (ticketInfo: any) => ipcRenderer.invoke('print-ticket', ticketInfo),
|
||||
|
||||
// 业务相关
|
||||
getBusinesses: () => ipcRenderer.invoke('get-businesses'),
|
||||
|
||||
// 窗口控制
|
||||
minimize: () => ipcRenderer.send('window-minimize'),
|
||||
maximize: () => ipcRenderer.send('window-maximize'),
|
||||
close: () => ipcRenderer.send('window-close'),
|
||||
|
||||
// 监听事件
|
||||
onWindowStateChange: (callback: (state: string) => void) => {
|
||||
ipcRenderer.on('window-state-changed', (_, state) => callback(state))
|
||||
},
|
||||
})
|
||||
|
||||
// 声明全局类型
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
printTicket: (ticketInfo: any) => Promise<any>
|
||||
getBusinesses: () => Promise<any>
|
||||
minimize: () => void
|
||||
maximize: () => void
|
||||
close: () => void
|
||||
onWindowStateChange: (callback: (state: string) => void) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,43 @@
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
|
||||
import { createPinia } from 'pinia'
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import { userLogin } from './services/authService'
|
||||
import './style.css'
|
||||
|
||||
// 应用启动初始化
|
||||
const initApp = async () => {
|
||||
try {
|
||||
// 执行自动登录
|
||||
const loginSuccess = await userLogin()
|
||||
|
||||
if (!loginSuccess) {
|
||||
console.warn('自动登录失败,应用将继续运行但部分功能可能受限')
|
||||
}
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
// 注册所有 Element Plus 图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(pinia)
|
||||
app.use(ElementPlus, {
|
||||
size: 'default',
|
||||
zIndex: 2000,
|
||||
})
|
||||
|
||||
app.mount('#app')
|
||||
} catch (error) {
|
||||
console.error('应用启动失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
createApp(App).mount('#app')
|
||||
// 启动应用
|
||||
initApp()
|
||||
|
||||
@ -0,0 +1,79 @@
|
||||
// src/utils/http.ts
|
||||
import type { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
||||
import axios from 'axios'
|
||||
|
||||
// 可选:定义通用响应结构(根据后端约定调整)
|
||||
interface ApiResponse<T = any> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://192.168.0.129:8845/api'
|
||||
|
||||
// 创建 axios 实例
|
||||
const service: AxiosInstance = axios.create({
|
||||
baseURL: baseURL, // 使用 Vite 环境变量
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
// 添加 token 示例
|
||||
const token = localStorage.getItem('access_token')
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response: AxiosResponse<ApiResponse>) => {
|
||||
const { code, message, data } = response.data
|
||||
|
||||
// 根据业务状态码判断
|
||||
if (code === 200) {
|
||||
return data // 直接返回 data
|
||||
} else {
|
||||
// 业务错误(如 token 失效、权限不足等)
|
||||
console.error('业务错误:', message)
|
||||
return Promise.reject(new Error(message))
|
||||
}
|
||||
},
|
||||
error => {
|
||||
// 网络错误或 HTTP 状态码非 2xx
|
||||
if (error.response) {
|
||||
// 服务器返回了错误状态码
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
// 未授权,跳转登录
|
||||
console.error('未授权的操作')
|
||||
break
|
||||
case 403:
|
||||
console.error('Forbidden')
|
||||
break
|
||||
case 500:
|
||||
console.error('服务器内部错误')
|
||||
break
|
||||
default:
|
||||
console.error('请求失败:', error.response.data)
|
||||
}
|
||||
} else if (error.request) {
|
||||
console.error('无响应:', error.request)
|
||||
} else {
|
||||
console.error('请求配置错误:', error.message)
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
||||
@ -0,0 +1,13 @@
|
||||
// 业务响应数据
|
||||
export interface BusinessItem {
|
||||
id: string
|
||||
uid: number
|
||||
tenantId: string
|
||||
deleted: boolean
|
||||
prefix: string
|
||||
name: string
|
||||
enabled?: number
|
||||
type: number
|
||||
handleCount: number
|
||||
isSpecial: number
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
// 登录请求参数
|
||||
export interface LoginRequest {
|
||||
username: string
|
||||
password: string
|
||||
tenantId: string
|
||||
}
|
||||
|
||||
// 登录响应数据
|
||||
export interface LoginResponse {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
tokenType: string
|
||||
expiresIn: number
|
||||
userInfo: {
|
||||
id: string
|
||||
username: string
|
||||
realName: string
|
||||
role: string
|
||||
tenantId: string
|
||||
uid: string
|
||||
image?: string
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,4 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
@ -1,26 +1,29 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
import electron from 'vite-plugin-electron'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), electron([
|
||||
{
|
||||
entry: 'src/electron/main/index.ts',
|
||||
vite:{
|
||||
build: {
|
||||
outDir: 'dist-electron/main'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
entry: 'src/electron/preload/index.ts',
|
||||
vite:{
|
||||
build: {
|
||||
outDir: 'dist-electron/preload'
|
||||
}
|
||||
}
|
||||
}
|
||||
])],
|
||||
plugins: [
|
||||
vue(),
|
||||
electron([
|
||||
{
|
||||
entry: 'src/electron/main/index.ts',
|
||||
vite: {
|
||||
build: {
|
||||
outDir: 'dist-electron/main',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
entry: 'src/electron/preload/index.ts',
|
||||
vite: {
|
||||
build: {
|
||||
outDir: 'dist-electron/preload',
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
],
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue