Browse Source

1226

master
lichong 4 months ago
commit
b30a01cfbe
  1. 9
      .editorconfig
  2. 5
      .gitignore
  3. 1
      .npmrc
  4. 6
      .prettierignore
  5. 6
      .prettierrc
  6. 34
      README.md
  7. 12
      build/entitlements.mac.plist
  8. BIN
      build/icon.icns
  9. BIN
      build/icon.ico
  10. 36
      build/notarize.js
  11. 43
      electron-builder.yml
  12. 28
      electron.vite.config.js
  13. 6
      jsconfig.json
  14. 41
      package.json
  15. BIN
      public/icon/icon.jpg
  16. 70
      src/main/index.js
  17. 21
      src/preload/dl.js
  18. 21
      src/preload/index.js
  19. 16
      src/renderer/index.html
  20. 90
      src/renderer/src/App.vue
  21. 9
      src/renderer/src/assets/css/base.css
  22. BIN
      src/renderer/src/assets/excel/19年1月销售汇总.xls
  23. BIN
      src/renderer/src/assets/excel/20年1月销售汇总.xls
  24. BIN
      src/renderer/src/assets/excel/21年1月销售汇总.xls
  25. BIN
      src/renderer/src/assets/excel/22年1月销售汇总.xls
  26. 8
      src/renderer/src/assets/js/db.js
  27. 97
      src/renderer/src/assets/json/sale.json
  28. 68
      src/renderer/src/components/formcomponent.vue
  29. 142
      src/renderer/src/components/tablecomponent.vue
  30. 33
      src/renderer/src/main.js
  31. 430
      src/renderer/src/views/dashboard.vue
  32. 584
      src/renderer/src/views/sale.vue

9
.editorconfig

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

5
.gitignore

@ -0,0 +1,5 @@
node_modules
dist
out
*.log*
package-lock.json

1
.npmrc

@ -0,0 +1 @@
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/

6
.prettierignore

@ -0,0 +1,6 @@
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json

6
.prettierrc

@ -0,0 +1,6 @@
{
"singleQuote": true,
"semi": false,
"printWidth": 100,
"trailingComma": "none"
}

34
README.md

@ -0,0 +1,34 @@
# my-app
An Electron application with Vue
## Recommended IDE Setup
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
## Project Setup
### Install(node 16.20.1)
```bash
$ npm install
```
### Development
```bash
$ npm run dev
```
### Build
```bash
# For windows
$ npm run build:win
# For macOS
$ npm run build:mac
# For Linux
$ npm run build:linux
```

12
build/entitlements.mac.plist

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>

BIN
build/icon.icns

Binary file not shown.

BIN
build/icon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

36
build/notarize.js

@ -0,0 +1,36 @@
const { notarize } = require('electron-notarize')
module.exports = async (context) => {
if (process.platform !== 'darwin') return
console.log('aftersign hook triggered, start to notarize app.')
if (!process.env.CI) {
console.log(`skipping notarizing, not in CI.`)
return
}
if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) {
console.warn('skipping notarizing, APPLE_ID and APPLE_ID_PASS env variables must be set.')
return
}
const appId = 'com.electron.app'
const { appOutDir } = context
const appName = context.packager.appInfo.productFilename
try {
await notarize({
appBundleId: appId,
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLEIDPASS
})
} catch (error) {
console.error(error)
}
console.log(`done notarizing ${appId}.`)
}

43
electron-builder.yml

@ -0,0 +1,43 @@
appId: com.electron.lichong
productName: 销售统计
directories:
buildResources: build
files:
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
asarUnpack:
- '**/*.{node,dll}'
afterSign: build/notarize.js
win:
executableName: lichong-app
nsis:
oneClick: false
artifactName: 销售统计-${version}-setup.${ext}
allowToChangeInstallationDirectory: true
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
entitlementsInherit: build/entitlements.mac.plist
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
dmg:
artifactName: ${name}-${version}.${ext}
linux:
target:
- AppImage
- snap
- deb
maintainer: electronjs.org
category: Utility
appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: generic
url: https://example.com/auto-updates

28
electron.vite.config.js

@ -0,0 +1,28 @@
import { resolve } from 'path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()]
},
preload: {
plugins: [externalizeDepsPlugin()],
build: {
rollupOptions: {
input: {
dl: resolve(__dirname, 'src/preload/dl.js'),
index: resolve(__dirname, 'src/preload/index.js')
}
}
}
},
renderer: {
resolve: {
alias: {
'@renderer': resolve('src/renderer/src')
}
},
plugins: [vue()]
}
})

6
jsconfig.json

@ -0,0 +1,6 @@
{
"exclude": [
"node_modules",
"public"
]
}

41
package.json

@ -0,0 +1,41 @@
{
"name": "electron",
"version": "1.0.1",
"description": "An Electron application with Vue",
"main": "./out/main/index.js",
"author": "lichong",
"homepage": "https://www.electronjs.org",
"scripts": {
"npmi": "npm i",
"dev": "electron-vite dev",
"build": "electron-vite build",
"build:win": "npm run build && electron-builder --win --config",
"build:mac": "npm run build && electron-builder --mac --config",
"build:linux": "npm run build && electron-builder --linux --config"
},
"dependencies": {
"@electron-toolkit/preload": "^1.0.2",
"@electron-toolkit/utils": "^1.0.2",
"dayjs": "^1.11.11",
"dexie": "^4.0.8",
"echarts": "^5.5.1",
"element-plus": "^2.7.1",
"lodash": "^4.17.21",
"vxe-pc-ui": "^4.2.26",
"vxe-table": "^4.7.94",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.2.0",
"@vitejs/plugin-vue": "^3.1.2",
"@vue/eslint-config-prettier": "^7.0.0",
"electron": "^20.3.2",
"electron-builder": "^23.6.0",
"electron-notarize": "^1.2.1",
"electron-vite": "^1.0.11",
"less": "^4.1.3",
"prettier": "^2.7.1",
"vite": "^3.1.8",
"vue": "^3.2.41"
}
}

BIN
public/icon/icon.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

70
src/main/index.js

@ -0,0 +1,70 @@
import { app, shell, BrowserWindow, nativeImage } from 'electron'
import * as path from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
// logo
const logoIcon = nativeImage.createFromPath(path.join(__dirname, '../../public/icon/icon.jpg'))
// 主窗口
let mainWindow
function createWindow() {
mainWindow = new BrowserWindow({
minWidth: 1366,
minHeight: 900,
height: 1260,
show: false,
autoHideMenuBar: true,
icon: logoIcon,
webPreferences: {
preload: path.resolve(__dirname, '../preload/index.js'),
sandbox: false,
nodeIntegration: true,
contextIsolation: false
}
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
// mainWindow.webContents.openDevTools()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// mainWindow.loadURL('http://localhost:5173/')
mainWindow.on('close', () => {
app.exit()
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
}
}
app.whenReady().then(() => {
electronApp.setAppUserModelId('com.electron')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.

21
src/preload/dl.js

@ -0,0 +1,21 @@
import { contextBridge, clipboard } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer
const api = {}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
contextBridge.exposeInMainWorld('elecClipboard', clipboard)
} catch (error) {
console.error(error)
}
} else {
window.electron = electronAPI
window.api = api
}

21
src/preload/index.js

@ -0,0 +1,21 @@
import { contextBridge, clipboard } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer
const api = {}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
contextBridge.exposeInMainWorld('elecClipboard', clipboard)
} catch (error) {
console.error(error)
}
} else {
window.electron = electronAPI
window.api = api
}

16
src/renderer/index.html

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>销售统计</title>
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self';img-src 'self' data:; style-src 'self' 'unsafe-inline';font-src 'self' data:;" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

90
src/renderer/src/App.vue

@ -0,0 +1,90 @@
<template>
<div class="appClass" v-if="isVip">
<el-tabs tab-position="left" class="appClassLeft" v-model="tabName">
<el-tab-pane label="销售统计" name="sale1">
<dashboardComponent></dashboardComponent>
</el-tab-pane>
<el-tab-pane label="供应商管理" name="sale6">
<saleComponent></saleComponent>
</el-tab-pane>
<el-tab-pane label="天猫小店泰和苑店" name="sale2">
<saleComponent></saleComponent>
</el-tab-pane>
<el-tab-pane label="天猫小店牡丹园店" name="sale3">
<saleComponent></saleComponent>
</el-tab-pane>
<el-tab-pane label="金叶便利东方红店" name="sale4">
<saleComponent></saleComponent>
</el-tab-pane>
<el-tab-pane label="天恩认养一头牛官旗店" name="sale5">
<saleComponent></saleComponent>
</el-tab-pane>
</el-tabs>
</div>
<div v-else class="noVip">
<h3>
<el-tag type="danger" class="noVipTag">体验已过期请联系管理员</el-tag>
</h3>
</div>
</template>
<script>
import _ from 'lodash'
import dayjs from 'dayjs'
import saleComponent from "./views/sale.vue"
import dashboardComponent from "./views/dashboard.vue"
export default {
name: 'app',
components: { saleComponent, dashboardComponent },
data() {
return {
_: _,
dayjs: dayjs,
isVip: false,
jihuoma: "",
zhucema: "",
tabName: "sale1"
}
},
watch: {},
computed: {},
methods: {
},
async mounted() {
let fiveDay = dayjs('2025-01-15T00:00:00').valueOf()
if (!this.isVip) {
if (dayjs().valueOf() > fiveDay) {
this.isVip = false
return
} else {
this.isVip = true
}
}
},
}
</script>
<style scoped>
.appClass {
width: 100vw;
height: 100vh;
position: relative;
}
.appClassLeft {
height: 100vh;
position: relative;
}
.noVip {
text-align: center;
margin-top: calc(50vh - 98px);
}
.noVipTag {
font-size: 48px;
line-height: 48px;
padding: 36px;
}
</style>

9
src/renderer/src/assets/css/base.css

@ -0,0 +1,9 @@
html,
body {
margin: 0;
padding: 0;
}
.is-left {
background-color: #999 !important;
}

BIN
src/renderer/src/assets/excel/19年1月销售汇总.xls

Binary file not shown.

BIN
src/renderer/src/assets/excel/20年1月销售汇总.xls

Binary file not shown.

BIN
src/renderer/src/assets/excel/21年1月销售汇总.xls

Binary file not shown.

BIN
src/renderer/src/assets/excel/22年1月销售汇总.xls

Binary file not shown.

8
src/renderer/src/assets/js/db.js

@ -0,0 +1,8 @@
import Dexie from 'dexie';
export const myDatabase = new Dexie('myDatabase');
// 定义数据库版本
myDatabase.version(2).stores({
sale: '++id, 货号, tableName, 品名, 供应商名称, 销售数量, 销售金额, 退货数量, 退货金额, 数量小计, 金额小计, create_at, update_at',
});

97
src/renderer/src/assets/json/sale.json

@ -0,0 +1,97 @@
[
{
"label": "货号",
"prop": "货号",
"type": "text",
"width": "130",
"isSort": true,
"tableShow": true,
"formShow": true
},
{
"label": "品名",
"prop": "品名",
"type": "text",
"isSort": true,
"tableShow": true,
"formShow": true
},
{
"label": "供应商名称",
"prop": "供应商名称",
"type": "text",
"isSort": true,
"tableShow": true,
"formShow": true
},
{
"label": "销售数量",
"prop": "销售数量",
"type": "text",
"width": "100",
"isSort": true,
"tableShow": true,
"formShow": true
},
{
"label": "销售金额",
"prop": "销售金额",
"type": "text",
"width": "100",
"isSort": true,
"tableShow": true,
"formShow": true
},
{
"label": "退货数量",
"prop": "退货数量",
"type": "text",
"width": "100",
"isSort": true,
"tableShow": true,
"formShow": true
},
{
"label": "退货金额",
"prop": "退货金额",
"type": "text",
"width": "100",
"isSort": true,
"tableShow": true,
"formShow": true
},
{
"label": "数量小计",
"prop": "数量小计",
"type": "text",
"width": "100",
"isSort": true,
"tableShow": true,
"formShow": true
},
{
"label": "金额小计",
"prop": "金额小计",
"type": "text",
"width": "100",
"isSort": true,
"tableShow": true,
"formShow": true
},
{
"label": "创建时间",
"prop": "create_at",
"type": "date",
"isSort": false,
"tableShow": false,
"formShow": false
},
{
"label": "更新时间",
"prop": "update_at",
"type": "date",
"isSort": false,
"tableShow": false,
"formShow": false
}
]

68
src/renderer/src/components/formcomponent.vue

@ -0,0 +1,68 @@
<template>
<div class="tableClass">
<el-form :model="formData" label-suffix="" :disabled="disabled" @submit.prevent>
<el-row>
<el-col :key="formIndex" v-for="(formItem, formIndex) in formHeader" :span="4">
<el-form-item :label="formItem.label">
<template v-if="['text', 'textarea'].includes(formItem.type)">
<el-input v-model="formData[formItem.prop]" :type="formItem.type" :autosize="{ minRows: 3, maxRows: 15 }"
style="width: 95%;" />
</template>
<template v-else-if="formItem.type === 'date'">
<el-date-picker v-model="formData[formItem.prop]" type="date"
:disabled="['create_at', 'update_at'].indexOf(formItem.prop) !== -1"
:placeholder="`请选择${formItem.label}`" />
</template>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</template>
<script>
import _, { max, min } from 'lodash'
import dayjs from 'dayjs'
import { ElMessage, ElMessageBox } from 'element-plus'
export default {
name: 'formcomponent',
components: {},
props: {
formHeader: {
type: Array,
default: () => {
return []
}
},
formData: {
type: Object,
default: () => {
return {}
}
},
disabled: {
type: Boolean,
default: () => {
return false
}
}
},
data() {
return {
_: _,
dayjs: dayjs,
}
},
methods: {},
async mounted() { },
watch: {},
computed: {}
}
</script>
<style scoped>
.tableClass {
text-align: left;
text-align-last: left;
}
</style>

142
src/renderer/src/components/tablecomponent.vue

@ -0,0 +1,142 @@
<template>
<div class="tableClass">
<el-auto-resizer>
<template #default="{ height, width }">
<vxe-table show-overflow :data="tableData" round :height="height" :scroll-y="{ enabled: true, gt: 0 }"
:checkbox-config="{ labelField: 'seq', highlight: true }" @checkbox-all="selectAllChangeEvent"
@checkbox-change="selectChange" ref="tableRef" border min-height="806px" :column-config="{ resizable: true }"
:footer-data="footerData" :show-footer="true">
<vxe-column type="checkbox" width="60"></vxe-column>
<vxe-column :field="headerItem.prop" :title="headerItem.label"
v-for="(headerItem, headerIndex) in tableHeader" :key="headerIndex" :sortable="headerItem.isSort"
:width="headerItem.width">
<template #default="{ row }">
<template v-if="headerItem.type === 'text'">
<span>{{ row[headerItem.prop] }}</span>
</template>
<template v-else-if="headerItem.type === 'date'">
<span>{{ dayjs(row[headerItem.prop]).format("YYYY-MM-DD HH:mm:ss") }}</span>
</template>
<template v-else>{{ row[headerItem.prop] }}</template>
</template>
</vxe-column>
<vxe-column title="操作" width="145">
<template #default="{ row }">
<el-button type="primary" circle @click="optClick(row, 'edit')">
<el-icon>
<Edit />
</el-icon>
</el-button>
<el-button type="info" circle @click="optClick(row, 'info')">
<el-icon>
<InfoFilled />
</el-icon>
</el-button>
<el-button type="danger" circle @click="optClick(row, 'del')">
<el-icon>
<Delete />
</el-icon>
</el-button>
</template>
</vxe-column>
</vxe-table>
<vxe-pager :current-page.sync="pageVO.currentPage" :page-size.sync="pageVO.pageSize" :total="pageVO.total"
@page-change="pageChange" :page-sizes="pageVO.pageSizes"
:layouts="['Home', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'End', 'Sizes', 'FullJump', 'Total']">
</vxe-pager>
</template>
</el-auto-resizer>
</div>
</template>
<script>
import _ from 'lodash'
import dayjs from 'dayjs'
export default {
name: 'tablecomponent',
components: {},
emits: ["selectChange", "edit", "info", "del", "pageChange"],
props: {
tableHeader: {
type: Array,
default: () => {
return []
}
},
pageVO: {
type: Object,
default: () => {
return {
total: 0,
currentPage: 1,
pageSize: 16,
pageSizes: [16, 30, 50, 100]
}
}
},
tableData: {
type: Array,
default: () => {
return []
}
},
},
data() {
return {
_: _,
dayjs: dayjs,
}
},
methods: {
clearSelection() {
const $table = this.$refs.tableRef
if ($table) {
$table.clearCheckboxRow()
}
},
selectChange({ checked }) {
let $table = this.$refs.tableRef
if ($table) {
let selection = $table.getCheckboxRecords()
this.$emit("selectChange", selection)
}
},
selectAllChangeEvent({ checked }) {
let $table = this.$refs.tableRef
if ($table) {
let selection = $table.getCheckboxRecords()
this.$emit("selectChange", selection)
}
},
optClick(row, type) {
this.$emit(type, row)
},
pageChange({ pageSize, currentPage }) {
this.$emit("pageChange", { pageSize, currentPage })
},
},
async mounted() { },
watch: {},
computed: {
footerData() {
return [{
"货号": '合计',
"销售数量": _.sumBy(this.tableData, '销售数量').toFixed(2),
"销售金额": _.sumBy(this.tableData, '销售金额').toFixed(2),
"退货数量": _.sumBy(this.tableData, '退货数量').toFixed(2),
"退货金额": _.sumBy(this.tableData, '退货金额').toFixed(2),
"数量小计": _.sumBy(this.tableData, '数量小计').toFixed(2),
"金额小计": _.sumBy(this.tableData, '金额小计').toFixed(2),
}]
}
}
}
</script>
<style scoped>
.tableClass {
height: calc(100vh - 100px);
text-align: center;
text-align-last: center;
}
</style>

33
src/renderer/src/main.js

@ -0,0 +1,33 @@
// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
import App from './App.vue'
import './assets/css/base.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import VxeUI from 'vxe-pc-ui'
import 'vxe-pc-ui/lib/style.css'
// ...
// 完整导入 表格库
import VxeUITable from 'vxe-table'
import 'vxe-table/lib/style.css'
const app = createApp(App)
// window.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = true
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(ElementPlus, {
locale: zhCn,
})
app.use(VxeUI).use(VxeUITable)
app.mount('#app')
document.addEventListener('keydown', (event) => {
const { ctrlKey, shiftKey, key } = event
if ((ctrlKey && shiftKey && key === 'I') || key === 'F12') {
return event.preventDefault()
}
})

430
src/renderer/src/views/dashboard.vue

@ -0,0 +1,430 @@
<template>
<div class="rightClass">
<el-row>
<el-col :span="24">
<div class="echartsClass1">
<div id="echarts1" class="echartsClass1"></div>
</div>
</el-col>
<el-col :span="12">
<div class="echartsClass2">
<div id="echarts2" class="echartsClass2"></div>
</div>
</el-col>
<el-col :span="12">
<div class="echartsClass2">
<div id="echarts3" class="echartsClass2"></div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import * as echarts from 'echarts';
import { markRaw } from "vue";
export default {
name: 'dashboard',
components: {},
watch: {},
computed: {},
data() {
return {
ecahrts1: null,
ecahrts2: null,
ecahrts3: null,
}
},
methods: {
init() {
this.dealEcharts1()
this.dealEcharts2()
this.dealEcharts3()
},
dealEcharts1() {
let option = {
color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],
title: {
text: '销售统计'
},
legend: {
data: ['第一季度', '第二季度', '第三季度', '第四季度']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: '第一季度',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 0
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgb(128, 255, 165)'
},
{
offset: 1,
color: 'rgb(1, 191, 236)'
}
])
},
emphasis: {
focus: 'series'
},
data: [140, 232, 101, 264, 90, 340, 250]
},
{
name: '第二季度',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 0
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgb(0, 221, 255)'
},
{
offset: 1,
color: 'rgb(77, 119, 255)'
}
])
},
emphasis: {
focus: 'series'
},
data: [120, 282, 111, 234, 220, 340, 310]
},
{
name: '第三季度',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 0
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgb(55, 162, 255)'
},
{
offset: 1,
color: 'rgb(116, 21, 219)'
}
])
},
emphasis: {
focus: 'series'
},
data: [320, 132, 201, 334, 190, 130, 220]
},
{
name: '第四季度',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 0
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgb(255, 0, 135)'
},
{
offset: 1,
color: 'rgb(135, 0, 157)'
}
])
},
emphasis: {
focus: 'series'
},
data: [220, 402, 231, 134, 190, 230, 120]
},
]
};
if (this.ecahrts1) {
this.ecahrts1.setOption(option);
} else {
let chartDom = document.getElementById('echarts1');
this.ecahrts1 = markRaw(echarts.init(chartDom), null, { renderer: 'webgl' })
}
this.ecahrts1.setOption(option);
},
dealEcharts2() {
let option = {
title: {
text: '销售统计'
},
grid: {
left: 30,
right: 110,
bottom: 30,
containLabel: true
},
xAxis: {
type: 'category',
splitLine: {
show: true
},
axisLabel: {
margin: 30,
fontSize: 16
},
boundaryGap: false,
data: [
'1月',
'2月',
'3月',
'4月',
'5月',
'6月',
'7月',
'8月',
'9月',
'10月',
'11月',
'12月'
]
},
yAxis: {
type: 'value',
axisLabel: {
margin: 30,
fontSize: 16,
formatter: '{value}万元'
}
},
series: [
{
name: '天猫小店泰和苑店',
symbolSize: 20,
type: 'line',
smooth: true,
emphasis: {
focus: 'series'
},
endLabel: {
show: true,
formatter: '{a}',
distance: 20
},
lineStyle: {
width: 4
},
data: [80, 30, 40, 10, 30, 40, 10, 30, 40, 20, 20, 10]
},
{
name: '天猫小店牡丹园店',
symbolSize: 20,
type: 'line',
smooth: true,
emphasis: {
focus: 'series'
},
endLabel: {
show: true,
formatter: '{a}',
distance: 20
},
lineStyle: {
width: 4
},
data: [30, 85, 74, 63, 85, 58, 85, 28, 58, 45, 78, 29]
},
{
name: '金叶便利东方红店',
symbolSize: 20,
type: 'line',
smooth: true,
emphasis: {
focus: 'series'
},
endLabel: {
show: true,
formatter: '{a}',
distance: 20
},
lineStyle: {
width: 4
},
data: [44, 52, 72, 24, 11, 82, 72, 61, 32, 71, 44, 53]
},
{
name: '天恩认养一头牛官旗店',
symbolSize: 20,
type: 'line',
smooth: true,
emphasis: {
focus: 'series'
},
endLabel: {
show: true,
formatter: '{a}',
distance: 20
},
lineStyle: {
width: 4
},
data: [82, 21, 53, 53, 64, 43, 34, 24, 41, 73, 53, 24]
}
]
};
if (this.ecahrts2) {
this.ecahrts2.setOption(option);
} else {
let chartDom = document.getElementById('echarts2');
this.ecahrts2 = markRaw(echarts.init(chartDom), null, { renderer: 'webgl' })
}
this.ecahrts2.setOption(option);
},
dealEcharts3() {
let option = {
"legend": {
"top": "bottom"
},
"toolbox": {
"show": true,
"feature": {
"mark": {
"show": true
},
"dataView": {
"show": true,
"readOnly": false
},
"restore": {
"show": true
},
"saveAsImage": {
"show": true
}
}
},
"series": [
{
"name": "店铺统计",
"type": "pie",
"radius": [
25,
100
],
"roseType": "area",
"itemStyle": {
"borderRadius": 8
},
"data": [
{
"value": 40,
"name": "天猫小店泰和苑店"
},
{
"value": 38,
"name": "天猫小店牡丹园店"
},
{
"value": 32,
"name": "金叶便利东方红店"
},
{
"value": 30,
"name": "天恩认养一头牛官旗店"
}
]
}
]
};
if (this.ecahrts3) {
this.ecahrts3.setOption(option);
} else {
let chartDom = document.getElementById('echarts3');
this.ecahrts3 = markRaw(echarts.init(chartDom), null, { renderer: 'webgl' })
}
this.ecahrts3.setOption(option);
},
resize() {
if (this.ecahrts1) {
this.ecahrts1.resize({
animation: {
duration: 2000,
easing: 'cubicInOut'
}
}, true)
}
if (this.ecahrts2) {
this.ecahrts2.resize({
animation: {
duration: 2000,
easing: 'cubicInOut'
}
}, true)
}
if (this.ecahrts3) {
this.ecahrts3.resize({
animation: {
duration: 2000,
easing: 'cubicInOut'
}
}, true)
}
}
},
async mounted() {
this.init()
window.addEventListener('resize', this.resize)
},
}
</script>
<style scoped>
.rightClass {
padding: 8px;
}
.echartsClass1 {
width: calc(100vw - 200px);
height: 500px;
}
.echartsClass2 {
width: calc(50vw - 100px);
height: 500px;
}
</style>

584
src/renderer/src/views/sale.vue

@ -0,0 +1,584 @@
<template>
<div class="rightClass">
<div>
<el-row style="margin-bottom: 8px;">
<el-col>
<el-button type="primary" @click="addData" size="large">
<el-icon>
<Plus />
</el-icon>
<span>新增</span>
</el-button>
<el-button type="danger" @click="deleteData(selectionData)" :disabled="selectionData.length == 0"
size="large">
<el-icon>
<Delete />
</el-icon>
<span>删除</span>
</el-button>
<el-button type="danger" @click="delAll" size="large">
<el-icon>
<Delete />
</el-icon>
<span>清空</span>
</el-button>
<el-button type="primary" @click="openImport" size="large">
<el-icon>
<Delete />
</el-icon>
<span>导入数据</span>
</el-button>
<el-button type="primary" @click="exportData" size="large">
<el-icon>
<Delete />
</el-icon>
<span>导出</span>
</el-button>
<el-select v-model="searchObj.shopName" placeholder="请选择店铺名" class="selectClass" size="large">
<el-option v-for="item in shopOptions" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
<el-select v-model="searchObj.month" placeholder="请选择月份" class="selectClass" size="large">
<el-option v-for="item in monthOptions" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
<el-button type="primary" @click="updateSeach" size="large" style="margin-left: 16px;">
<el-icon>
<Delete />
</el-icon>
<span>搜索</span>
</el-button>
</el-col>
</el-row>
</div>
<div v-loading="loading">
<tablecomponent :tableHeader="tableHeader" :tableData="tableData" @selectChange="selectChange" @edit="edit"
@info="info" @del="del" @pageChange="pageChange" ref="tableComponentRef" :pageVO="pageVO">
</tablecomponent>
</div>
<el-dialog v-model="dialogFrom.visible" :title="dialogFrom.title" width="80%" :close-on-click-modal="false">
<formcomponent :formHeader="dialogFrom.formHeader" :formData="dialogFrom.formData"
:disabled="dialogFrom.disabled">
</formcomponent>
<template #footer>
<div>
<el-button @click="cancelDialog">取消</el-button>
<el-button type="primary" @click="submitDialog"> 确认 </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="importShow" title="导入数据" width="80%" :close-on-click-modal="false">
<div>
<el-select v-model="importObj.shopName" placeholder="请选择店铺名" class="selectClass" size="large">
<el-option v-for="item in shopOptions" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
<el-select v-model="importObj.month" placeholder="请选择月份" class="selectClass" size="large">
<el-option v-for="item in monthOptionsList" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
<el-upload :show-file-list="false" v-model="fileOriData" :before-upload="beforeAvatarUpload"
:http-request="successSubmit" accept=".xls,.xlsx,.csv"
style="display: inline-block;position: relative;top: 3px;left: 12px;margin-right: 24px;" multiple>
<el-button type="success" size="large">
<el-icon>
<Upload />
</el-icon>
<span>上传表格</span>
</el-button>
</el-upload>
</div>
<template #footer>
<div>
<el-button @click="cancelDialog">取消</el-button>
<el-button type="primary" @click="submitImport" :disabled="importCount === 0">
确认({{ importCount }})
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import _ from 'lodash'
import * as XLSX from 'xlsx';
import tableHeaderLocal from '../assets/json/sale.json'
import { myDatabase } from '../assets/js/db.js'
import dayjs from 'dayjs'
import tablecomponent from "../components/tablecomponent.vue"
import formcomponent from "../components/formcomponent.vue"
import { ElMessage, ElMessageBox } from 'element-plus'
export default {
name: 'sale',
components: { tablecomponent, formcomponent },
watch: {},
computed: {},
data() {
return {
_: _,
dayjs: dayjs,
tableHeader: [],
formHeader: [],
tableData: [],
selectionData: [],
importShow: false,
dialogFrom: {
visible: false,
title: "新增数据堆",
type: "add",
formHeader: [],
formData: {}
},
fileData: [],
pageVO: {
total: 0,
currentPage: 1,
pageSize: 16,
pageSizes: [16, 30, 50, 100]
},
searchObj: {
shopName: "",
month: ""
},
importObj: {
shopName: "",
month: ""
},
shopOptions: [
{
label: "天猫小店泰和苑店",
value: "1"
},
{
label: "天猫小店牡丹园店",
value: "2"
},
{
label: "金叶便利东方红店",
value: "3"
},
{
label: "天恩认养一头牛官旗店",
value: "4"
}
],
monthOptions: [
{
label: "全部",
value: ""
},
{
label: "1月",
value: "1月"
},
{
label: "2月",
value: "2月"
},
{
label: "3月",
value: "3月"
},
{
label: "4月",
value: "4月"
},
{
label: "5月",
value: "5月"
},
{
label: "6月",
value: "6月"
},
{
label: "7月",
value: "7月"
},
{
label: "8月",
value: "8月"
},
{
label: "9月",
value: "9月"
},
{
label: "10月",
value: "10月"
},
{
label: "11月",
value: "11月"
},
{
label: "12月",
value: "12月"
},
],
monthOptionsList: [
{
label: "1月",
value: "1月"
},
{
label: "2月",
value: "2月"
},
{
label: "3月",
value: "3月"
},
{
label: "4月",
value: "4月"
},
{
label: "5月",
value: "5月"
},
{
label: "6月",
value: "6月"
},
{
label: "7月",
value: "7月"
},
{
label: "8月",
value: "8月"
},
{
label: "9月",
value: "9月"
},
{
label: "10月",
value: "10月"
},
{
label: "11月",
value: "11月"
},
{
label: "12月",
value: "12月"
},
],
tableName: "",
loading: false,
fileOriData: null,
importCount: 0,
}
},
methods: {
openImport() {
this.importCount = 0
this.fileData = []
this.importShow = true
},
beforeAvatarUpload(rawFile) {
let imgList = ['text/csv', 'application/vnd.ms-excel', "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]
if (imgList.indexOf(rawFile.type) === -1) {
this.$msgbox.alert('请上传excel,csv格式的表格文件!')
return false
} else if (rawFile.size / 1024 / 1024 > 50) {
this.$msgbox.alert('表格文件的大小为小于50MB,数据过多时会处理过慢')
return true
}
return true
},
//
async successSubmit(opts) {
let that = this
let file = opts.file
this.fileDealData = []
let fileReader = new FileReader()
fileReader.onload = async function (e) {
let data = this.result
let workbook = XLSX.read(data, { type: 'binary' })
let sheetName = workbook.SheetNames[0]
let sheetData = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName])
that.fileData = _.concat(that.fileData, sheetData)
that.importCount++
}
fileReader.onerror = function (error) {
ElMessage({
message: `Error reading file:, ${error}`,
type: 'error',
})
}
fileReader.readAsArrayBuffer(file)
},
//
async updateSeach() {
this.loading = true
let collection = myDatabase.sale
let shopNameTemp = _.trim(_.trim(this.searchObj.shopName))
let monthTemp = _.trim(_.trim(this.searchObj.month))
if (shopNameTemp) {
collection = collection.filter((equip) => equip.shopName === shopNameTemp)
}
if (monthTemp) {
collection = collection.filter((equip) => equip.month === monthTemp)
}
this.pageVO.total = await collection.count()
this.tableData = await collection.offset((this.pageVO.currentPage - 1) * this.pageVO.pageSize).limit(this.pageVO.pageSize).toArray()
this.pageVO.pageSizes[5] = this.pageVO.total
this.loading = false
},
//
selectChange(selection) {
this.selectionData = _.cloneDeep(selection)
},
//
async exportData() {
let listCopy = _.cloneDeep(this.tableData)
if (listCopy.length) {
let allDataList = []
for (let i = 0; i < listCopy.length; i++) {
let listItem = listCopy[i];
let item = {}
for (let j = 0; j < this.tableHeader.length; j++) {
let headerItem = this.tableHeader[j];
item[headerItem.label] = listItem[headerItem.prop]
}
allDataList.push({ ...item })
}
let jsonWorkSheet = XLSX.utils.json_to_sheet(allDataList);
let workBook = {
SheetNames: ["sheet1"],
Sheets: {
["sheet1"]: jsonWorkSheet,
}
};
XLSX.writeFile(workBook, `销售表格${dayjs().format("YYYY-MM-DD_HH-mm-ss")}.xlsx`);
} else {
ElMessage({
type: 'error',
message: '当前表格无数据,请有数据后在导出',
})
}
},
//
async pageChange({ pageSize, currentPage }) {
this.pageVO.currentPage = currentPage
this.pageVO.pageSize = pageSize
await this.updateSeach()
},
//
cancelDialog() {
this.importCount = 0
this.fileData = []
this.importShow = false
this.dialogFrom = {
visible: false,
title: "新增",
type: "add",
formHeader: [],
formData: {}
}
},
//
async submitImport() {
let that = this
let dealDataObj = {
"销售数量": 1.2,
"销售金额": 1.2,
"退货数量": 1,
"退货金额": 1,
"数量小计": 1.2,
"金额小计": 1.2,
}
let allList = []
for (let i = 0; i < this.fileData.length; i++) {
let element = this.fileData[i];
let item = {}
for (let key in element) {
item[_.trim(key)] = _.trim(element[key])
}
if (item["行号"] && item["品名"] && item["供应商名称"]) {
for (let key in item) {
if (dealDataObj[_.trim(key)]) {
item[_.trim(key)] = Number((Number(_.trim(element[key])) * dealDataObj[_.trim(key)]).toFixed(2))
}
}
allList.push({
shopName: that.importObj.shopName,
month: that.importObj.month,
...item,
create_at: dayjs().format('YYYY-MM-DD HH:mm:ss'),
update_at: dayjs().format('YYYY-MM-DD HH:mm:ss')
})
}
}
try {
await myDatabase.sale.bulkAdd(allList)
this.cancelDialog()
await that.updateSeach()
} catch (error) {
ElMessage({
message: `销售数据重复,error:${error}`,
type: 'error',
})
}
await that.updateSeach()
},
//
async submitDialog() {
let params = { ...this.dialogFrom.formData, }
try {
await myDatabase.sale[this.dialogFrom.type]({ ...params, update_at: dayjs().format('YYYY-MM-DD HH:mm:ss') })
await this.updateSeach()
this.cancelDialog()
} catch (e) {
ElMessage(
{
type: "error",
message: "时间不允许重复,请重新设置时间"
}
)
}
},
//
addData() {
let formData = {}
for (let i = 0; i < this.formHeader.length; i++) {
let element = this.formHeader[i];
if (element.type === "text") {
formData[element.prop] = ""
} else if (element.type === "date") {
formData[element.prop] = dayjs().valueOf()
}
}
formData["create_at"] = dayjs().valueOf()
this.dialogFrom = {
visible: true,
title: "新增",
type: "add",
disabled: false,
formHeader: this.formHeader,
formData
}
},
//
edit(row) {
this.dialogFrom = {
visible: true,
title: `编辑${row['货号']}`,
type: "put",
disabled: false,
formHeader: this.formHeader,
formData: { ...row }
}
},
//
info(row) {
this.dialogFrom = {
visible: true,
title: `查看${row['货号']}`,
type: "info",
disabled: true,
formHeader: this.formHeader,
formData: { ...row }
}
},
//
del(row) {
this.deleteData([row])
},
//
deleteData(delList) {
let tooltipList = []
let idList = []
for (let i = 0; i < delList.length; i++) {
let element = delList[i];
tooltipList.push(element["货号"])
idList.push(element.id)
}
ElMessageBox.confirm(
`是否删除(${_.join(tooltipList, ",")})?`,
'danger',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'danger',
}
)
.then(async () => {
this.$refs.tableComponentRef.clearSelection()
await myDatabase.sale.bulkDelete(idList)
await this.updateSeach()
})
.catch((err) => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
},
//
delAll() {
ElMessageBox.confirm(
`是否删除全部数据?此操作不可逆!`,
'危险操作',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'danger',
}
)
.then(async () => {
this.$refs.tableComponentRef.clearSelection()
await myDatabase.sale.clear()
await this.updateSeach()
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
},
},
async mounted() {
this.tableHeader = _.filter(tableHeaderLocal, o => o.tableShow)
this.formHeader = _.filter(tableHeaderLocal, o => o.formShow)
await this.updateSeach()
},
}
</script>
<style scoped>
.rightClass {
padding: 8px;
}
.uploadClass {
top: 3px;
}
.inputClass {
display: inline-block;
margin: 0 8px;
position: relative;
}
.tagClass {
display: inline-block;
margin: 0 8px;
width: 200px;
position: relative;
}
.selectClass {
display: inline-block;
max-width: 200px;
margin-left: 16px;
}
</style>
Loading…
Cancel
Save