呼号端初版
parent
5a11975bd5
commit
80d82aaee4
@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"sqltools.connections": [
|
"sqltools.connections": [
|
||||||
{
|
{
|
||||||
"connectionTimeout": 15,
|
"connectionTimeout": 15,
|
||||||
"mssqlOptions": {
|
"mssqlOptions": {
|
||||||
"appName": "SQLTools",
|
"appName": "SQLTools",
|
||||||
"useUTC": true,
|
"useUTC": true,
|
||||||
"encrypt": true,
|
"encrypt": true,
|
||||||
"trustServerCertificate": false
|
"trustServerCertificate": false
|
||||||
},
|
},
|
||||||
"ssh": "Disabled",
|
"ssh": "Disabled",
|
||||||
"previewLimit": 50,
|
"previewLimit": 50,
|
||||||
"server": "47.96.78.103",
|
"server": "47.96.78.103",
|
||||||
"port": 1433,
|
"port": 1433,
|
||||||
"askForPassword": true,
|
"askForPassword": true,
|
||||||
"driver": "MSSQL",
|
"driver": "MSSQL",
|
||||||
"name": "QS aliyun",
|
"name": "QS aliyun",
|
||||||
"username": "sa",
|
"username": "sa",
|
||||||
"database": "QueuingSystem"
|
"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 vue from '@vitejs/plugin-vue'
|
||||||
|
import { defineConfig } from 'electron-vite'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: {
|
main: {},
|
||||||
plugins: [externalizeDepsPlugin(), bytecodePlugin()]
|
preload: {},
|
||||||
},
|
|
||||||
preload: {
|
|
||||||
plugins: [externalizeDepsPlugin(), bytecodePlugin()]
|
|
||||||
},
|
|
||||||
renderer: {
|
renderer: {
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
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'
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
electron: ElectronAPI
|
|
||||||
api: unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
declare module '*.vue' {
|
export {}
|
||||||
import type { DefineComponent } from 'vue'
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
interface IWinControl {
|
||||||
const component: DefineComponent<{}, {}, any>
|
windowMinimize: () => void
|
||||||
export default component
|
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 './assets/main.css'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import ElementPlus from 'element-plus'
|
||||||
import App from './App.vue'
|
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 Login from '@renderer/views/Login.vue'
|
||||||
import Main from '@renderer/views/Main.vue'
|
import Main from '@renderer/views/Main.vue'
|
||||||
|
import TicketList from '@renderer/views/TicketList.vue'
|
||||||
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
export default createRouter({
|
export default createRouter({
|
||||||
history: createWebHashHistory(), // hash模式
|
history: createWebHashHistory(), // hash模式
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', component: Login },
|
{ path: '/', component: Login },
|
||||||
{ path: '/login', 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": [],
|
"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 { app, BrowserWindow, ipcMain } from "electron";
|
||||||
import { fileURLToPath as t } from "url";
|
import path from "path";
|
||||||
import o from "path";
|
import { fileURLToPath } from "url";
|
||||||
const a = t(import.meta.url), i = o.dirname(a);
|
const __filename$1 = fileURLToPath(import.meta.url);
|
||||||
e.whenReady().then(() => {
|
const __dirname$1 = path.dirname(__filename$1);
|
||||||
const n = new r({
|
let mainWindow = null;
|
||||||
width: 800,
|
const isDev = process.env.NODE_ENV === "development";
|
||||||
height: 600,
|
const createWindow = () => {
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 600,
|
||||||
|
height: 800,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: !1,
|
nodeIntegration: false,
|
||||||
contextIsolation: !0,
|
contextIsolation: true
|
||||||
preload: o.join(i, "../preload/index.js")
|
// 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", () => {
|
ipcMain.handle("print-ticket", async (event, ticketInfo) => {
|
||||||
process.platform !== "darwin" && e.quit();
|
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";
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
o.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
// 可以在这里添加需要暴露的API
|
// 打印相关
|
||||||
platform: process.platform
|
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>
|
<template>
|
||||||
<div>
|
<HomeView />
|
||||||
<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" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<script setup lang="ts">
|
||||||
.logo {
|
import HomeView from '../src/views/HomeView.vue'
|
||||||
height: 6em;
|
</script>
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
<style lang="scss">
|
||||||
transition: filter 300ms;
|
* {
|
||||||
|
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>
|
</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 { app, BrowserWindow, ipcMain } from 'electron'
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = path.dirname(__filename)
|
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(() => {
|
app.whenReady().then(() => {
|
||||||
const win = new BrowserWindow({
|
createWindow()
|
||||||
width: 800,
|
|
||||||
height: 600,
|
app.on('activate', function () {
|
||||||
webPreferences: {
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||||
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'))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit()
|
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, ipcRenderer } from 'electron'
|
||||||
import { contextBridge } from 'electron'
|
|
||||||
|
|
||||||
// 暴露API给渲染进程
|
// 暴露安全的API给渲染进程
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
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 { createApp } from 'vue'
|
||||||
import './style.css'
|
|
||||||
import App from './App.vue'
|
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": [],
|
"files": [],
|
||||||
"references": [
|
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||||
{ "path": "./tsconfig.app.json" },
|
|
||||||
{ "path": "./tsconfig.node.json" }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +1,29 @@
|
|||||||
import { defineConfig } from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
import electron from 'vite-plugin-electron'
|
import electron from 'vite-plugin-electron'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue(), electron([
|
plugins: [
|
||||||
{
|
vue(),
|
||||||
entry: 'src/electron/main/index.ts',
|
electron([
|
||||||
vite:{
|
{
|
||||||
build: {
|
entry: 'src/electron/main/index.ts',
|
||||||
outDir: 'dist-electron/main'
|
vite: {
|
||||||
}
|
build: {
|
||||||
}
|
outDir: 'dist-electron/main',
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
entry: 'src/electron/preload/index.ts',
|
},
|
||||||
vite:{
|
{
|
||||||
build: {
|
entry: 'src/electron/preload/index.ts',
|
||||||
outDir: 'dist-electron/preload'
|
vite: {
|
||||||
}
|
build: {
|
||||||
}
|
outDir: 'dist-electron/preload',
|
||||||
}
|
},
|
||||||
])],
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue