Compare commits

...

2 Commits

Author SHA1 Message Date
cst 15b85dd9c1 feat: add merge 2026-07-17 09:26:50 +08:00
cst 5c7b605436 fix: add iconfont to eslintignore, fix commitlint config 2026-07-17 08:51:48 +08:00
46 changed files with 4914 additions and 247 deletions

View File

@ -4,3 +4,4 @@ public
*.config.js
*.config.ts
start-debug.js
src/assets/iconfont

View File

@ -12,7 +12,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "vscode.css-language-features"
},
"files.associations": {
"*.vue": "vue"

17
babel.config.js Normal file
View File

@ -0,0 +1,17 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
chrome: '80',
firefox: '75',
safari: '14',
edge: '80',
},
useBuiltIns: 'usage',
corejs: 3,
},
],
],
};

145
package-lock.json generated
View File

@ -1,14 +1,15 @@
{
"name": "cpms_web_admin_pc",
"version": "1.0.0",
"name": "cpms_operation_platform",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cpms_web_admin_pc",
"version": "1.0.0",
"name": "cpms_operation_platform",
"version": "0.0.0",
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@yp-component/root": "^0.0.46",
"ant-design-vue": "^4.0.0",
"express": "^5.2.1",
"html2canvas": "^1.4.1",
@ -21,20 +22,21 @@
"@commitlint/config-conventional": "^20.4.3",
"@types/node": "^20.10.0",
"@types/qs": "^6.15.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-vue": "^4.5.0",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"eslint": "^8.0.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.0.0",
"eslint-plugin-vue": "^9.33.0",
"husky": "^9.1.7",
"less": "^4.6.4",
"lint-staged": "^15.0.0",
"prettier": "^3.0.0",
"typescript": "^5.3.0",
"vite": "^5.0.0",
"vue-eslint-parser": "^10.4.1",
"vue-tsc": "^2.0.0"
},
"engines": {
@ -984,6 +986,13 @@
"url": "https://ko-fi.com/dangreen"
}
},
"node_modules/@types/esrecurse": {
"version": "4.3.1",
"resolved": "http://npm.nps.yunvip123.cn/@types/esrecurse/-/esrecurse-4.3.1.tgz",
"integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.9",
"dev": true,
@ -1014,6 +1023,8 @@
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.21.0",
"resolved": "http://npm.nps.yunvip123.cn/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
"integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1048,6 +1059,8 @@
},
"node_modules/@typescript-eslint/parser": {
"version": "6.21.0",
"resolved": "http://npm.nps.yunvip123.cn/@typescript-eslint/parser/-/parser-6.21.0.tgz",
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@ -1427,6 +1440,15 @@
"version": "3.5.39",
"license": "MIT"
},
"node_modules/@yp-component/root": {
"version": "0.0.46",
"resolved": "http://npm.nps.yunvip123.cn/@yp-component/root/-/root-0.0.46.tgz",
"integrity": "sha512-umgaFa2x/gEtlvXsrqbu9lgcUU/j8K8Jj4p1n45vJK6c5ysBMQMj6sYWT0Jr8YO65ofh9BSMPl17n8jlknY5xA==",
"dependencies": {
"cropperjs": "^1.6.2",
"vue": "^3.5.25"
}
},
"node_modules/accepts": {
"version": "2.0.0",
"license": "MIT",
@ -2034,6 +2056,12 @@
"typescript": ">=5"
}
},
"node_modules/cropperjs": {
"version": "1.6.2",
"resolved": "http://npm.nps.yunvip123.cn/cropperjs/-/cropperjs-1.6.2.tgz",
"integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"dev": true,
@ -2699,6 +2727,9 @@
},
"node_modules/eslint": {
"version": "8.57.1",
"resolved": "http://npm.nps.yunvip123.cn/eslint/-/eslint-8.57.1.tgz",
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2793,6 +2824,8 @@
},
"node_modules/eslint-plugin-vue": {
"version": "9.33.0",
"resolved": "http://npm.nps.yunvip123.cn/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz",
"integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2812,6 +2845,31 @@
"eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
}
},
"node_modules/eslint-plugin-vue/node_modules/vue-eslint-parser": {
"version": "9.4.3",
"resolved": "http://npm.nps.yunvip123.cn/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
"integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4",
"eslint-scope": "^7.1.1",
"eslint-visitor-keys": "^3.3.0",
"espree": "^9.3.1",
"esquery": "^1.4.0",
"lodash": "^4.17.21",
"semver": "^7.3.6"
},
"engines": {
"node": "^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
},
"peerDependencies": {
"eslint": ">=6.0.0"
}
},
"node_modules/eslint-scope": {
"version": "7.2.2",
"dev": true,
@ -5748,26 +5806,77 @@
}
},
"node_modules/vue-eslint-parser": {
"version": "9.4.3",
"version": "10.4.1",
"resolved": "http://npm.nps.yunvip123.cn/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz",
"integrity": "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4",
"eslint-scope": "^7.1.1",
"eslint-visitor-keys": "^3.3.0",
"espree": "^9.3.1",
"esquery": "^1.4.0",
"lodash": "^4.17.21",
"semver": "^7.3.6"
"debug": "^4.4.0",
"eslint-scope": "^8.2.0 || ^9.0.0",
"eslint-visitor-keys": "^4.2.0 || ^5.0.0",
"espree": "^10.3.0 || ^11.0.0",
"esquery": "^1.6.0",
"semver": "^7.6.3"
},
"engines": {
"node": "^14.17.0 || >=16.0.0"
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
},
"peerDependencies": {
"eslint": ">=6.0.0"
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0"
}
},
"node_modules/vue-eslint-parser/node_modules/eslint-scope": {
"version": "9.1.2",
"resolved": "http://npm.nps.yunvip123.cn/eslint-scope/-/eslint-scope-9.1.2.tgz",
"integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@types/esrecurse": "^4.3.1",
"@types/estree": "^1.0.8",
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "http://npm.nps.yunvip123.cn/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/vue-eslint-parser/node_modules/espree": {
"version": "11.2.0",
"resolved": "http://npm.nps.yunvip123.cn/espree/-/espree-11.2.0.tgz",
"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.16.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^5.0.1"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/vue-router": {

View File

@ -33,20 +33,21 @@
"@commitlint/config-conventional": "^20.4.3",
"@types/node": "^20.10.0",
"@types/qs": "^6.15.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-vue": "^4.5.0",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"eslint": "^8.0.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.0.0",
"eslint-plugin-vue": "^9.33.0",
"husky": "^9.1.7",
"less": "^4.6.4",
"lint-staged": "^15.0.0",
"prettier": "^3.0.0",
"typescript": "^5.3.0",
"vite": "^5.0.0",
"vue-eslint-parser": "^10.4.1",
"vue-tsc": "^2.0.0"
},
"lint-staged": {

View File

@ -12,7 +12,17 @@ export default defineComponent({
setup() {
const theme = {
token: {
colorPrimary: '#1677ff',
colorPrimary: '#1890ff',
colorSuccess: '#52c41a',
colorWarning: '#faad14',
colorError: '#ff4d4f',
colorInfo: '#1890ff',
colorBgBase: '#ffffff',
colorBgContainer: '#ffffff',
colorBgElevated: '#ffffff',
colorBgLayout: '#f0f2f5',
colorTextBase: '#262626',
borderRadius: 6,
},
};

View File

@ -0,0 +1,545 @@
/* Logo 字体 */
@font-face {
font-family: 'iconfont logo';
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src:
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix')
format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont')
format('svg');
}
.logo {
font-family: 'iconfont logo';
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition:
font-size 0.25s linear,
width 0.25s linear;
-moz-transition:
font-size 0.25s linear,
width 0.25s linear;
transition:
font-size 0.25s linear,
width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown > p,
.markdown > blockquote,
.markdown > .highlight,
.markdown > ol,
.markdown > ul {
width: 80%;
}
.markdown ul > li {
list-style: circle;
}
.markdown > ul li,
.markdown blockquote ul > li {
margin-left: 20px;
padding-left: 4px;
}
.markdown > ul li p,
.markdown > ol li p {
margin: 0.6em 0;
}
.markdown ol > li {
list-style: decimal;
}
.markdown > ol li,
.markdown blockquote ol > li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown > table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown > table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown > table th,
.markdown > table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown > table th {
background: #f7f7f7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown > br,
.markdown > p > br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*='language-'],
pre[class*='language-'] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*='language-']::-moz-selection,
pre[class*='language-'] ::-moz-selection,
code[class*='language-']::-moz-selection,
code[class*='language-'] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*='language-']::selection,
pre[class*='language-'] ::selection,
code[class*='language-']::selection,
code[class*='language-'] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*='language-'],
pre[class*='language-'] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
}
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background: #f5f2f0;
}
/* Inline code */
:not(pre) > code[class*='language-'] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: 0.7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, 0.5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #dd4a68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,380 @@
{
"id": "4533260",
"name": "改版-六个羽友",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "43518948",
"name": "场馆",
"font_class": "changguan1",
"unicode": "e87b",
"unicode_decimal": 59515
},
{
"icon_id": "31394058",
"name": "灯-点亮",
"font_class": "deng-dianliang",
"unicode": "e7b9",
"unicode_decimal": 59321
},
{
"icon_id": "31394059",
"name": "灯-关闭",
"font_class": "deng-guanbi",
"unicode": "e7ba",
"unicode_decimal": 59322
},
{
"icon_id": "43469992",
"name": "刷新",
"font_class": "shuaxin",
"unicode": "e878",
"unicode_decimal": 59512
},
{
"icon_id": "43469991",
"name": "关闭",
"font_class": "guanbi1",
"unicode": "e879",
"unicode_decimal": 59513
},
{
"icon_id": "39274830",
"name": "添加",
"font_class": "tianjia1",
"unicode": "e81f",
"unicode_decimal": 59423
},
{
"icon_id": "41007799",
"name": "签到码",
"font_class": "qiandaoma",
"unicode": "e857",
"unicode_decimal": 59479
},
{
"icon_id": "40996457",
"name": "日历",
"font_class": "rili1",
"unicode": "e856",
"unicode_decimal": 59478
},
{
"icon_id": "40820828",
"name": "分组title",
"font_class": "fenzutitle",
"unicode": "e84c",
"unicode_decimal": 59468
},
{
"icon_id": "40820822",
"name": "成功",
"font_class": "chenggong",
"unicode": "e84d",
"unicode_decimal": 59469
},
{
"icon_id": "40820826",
"name": "存草稿",
"font_class": "cuncaogao",
"unicode": "e84e",
"unicode_decimal": 59470
},
{
"icon_id": "40820824",
"name": "图片",
"font_class": "tupian",
"unicode": "e84f",
"unicode_decimal": 59471
},
{
"icon_id": "40820823",
"name": "微信支付",
"font_class": "weixinzhifu1",
"unicode": "e850",
"unicode_decimal": 59472
},
{
"icon_id": "40820821",
"name": "删除",
"font_class": "shanchu",
"unicode": "e851",
"unicode_decimal": 59473
},
{
"icon_id": "40820818",
"name": "钱包",
"font_class": "qianbao",
"unicode": "e852",
"unicode_decimal": 59474
},
{
"icon_id": "40820816",
"name": "男",
"font_class": "nan",
"unicode": "e853",
"unicode_decimal": 59475
},
{
"icon_id": "40820819",
"name": "金币",
"font_class": "jinbi",
"unicode": "e854",
"unicode_decimal": 59476
},
{
"icon_id": "40820817",
"name": "女",
"font_class": "nv",
"unicode": "e855",
"unicode_decimal": 59477
},
{
"icon_id": "40820832",
"name": "分享",
"font_class": "fenxiang1",
"unicode": "e847",
"unicode_decimal": 59463
},
{
"icon_id": "40820825",
"name": "修改",
"font_class": "xiugai",
"unicode": "e848",
"unicode_decimal": 59464
},
{
"icon_id": "40820830",
"name": "编辑",
"font_class": "bianji",
"unicode": "e849",
"unicode_decimal": 59465
},
{
"icon_id": "40820829",
"name": "更多1",
"font_class": "gengduo1",
"unicode": "e84a",
"unicode_decimal": 59466
},
{
"icon_id": "40820820",
"name": "上",
"font_class": "shang",
"unicode": "e84b",
"unicode_decimal": 59467
},
{
"icon_id": "40820837",
"name": "添加",
"font_class": "tianjia",
"unicode": "e840",
"unicode_decimal": 59456
},
{
"icon_id": "40820836",
"name": "更多",
"font_class": "gengduo",
"unicode": "e841",
"unicode_decimal": 59457
},
{
"icon_id": "40820835",
"name": "电话",
"font_class": "dianhua",
"unicode": "e842",
"unicode_decimal": 59458
},
{
"icon_id": "40820827",
"name": "撤销",
"font_class": "chexiao",
"unicode": "e843",
"unicode_decimal": 59459
},
{
"icon_id": "40820831",
"name": "微信",
"font_class": "weixin",
"unicode": "e844",
"unicode_decimal": 59460
},
{
"icon_id": "40820834",
"name": "分组",
"font_class": "fenzu",
"unicode": "e845",
"unicode_decimal": 59461
},
{
"icon_id": "40820833",
"name": "点名",
"font_class": "dianming",
"unicode": "e846",
"unicode_decimal": 59462
},
{
"icon_id": "36759285",
"name": "套餐",
"font_class": "taocan",
"unicode": "e7fc",
"unicode_decimal": 59388
},
{
"icon_id": "33120817",
"name": "特价",
"font_class": "tejia",
"unicode": "e7da",
"unicode_decimal": 59354
},
{
"icon_id": "33120818",
"name": "满减",
"font_class": "manjian",
"unicode": "e7db",
"unicode_decimal": 59355
},
{
"icon_id": "40351403",
"name": "票",
"font_class": "piao",
"unicode": "e83f",
"unicode_decimal": 59455
},
{
"icon_id": "848275",
"name": "勾",
"font_class": "gou",
"unicode": "e63d",
"unicode_decimal": 58941
},
{
"icon_id": "40248532",
"name": "分享",
"font_class": "fenxiang",
"unicode": "e83e",
"unicode_decimal": 59454
},
{
"icon_id": "40216674",
"name": "浏览量",
"font_class": "liulanliang",
"unicode": "e83c",
"unicode_decimal": 59452
},
{
"icon_id": "40216675",
"name": "日历",
"font_class": "rili",
"unicode": "e83d",
"unicode_decimal": 59453
},
{
"icon_id": "40191312",
"name": "左",
"font_class": "zuo",
"unicode": "e83b",
"unicode_decimal": 59451
},
{
"icon_id": "40191242",
"name": "下",
"font_class": "xia",
"unicode": "e83a",
"unicode_decimal": 59450
},
{
"icon_id": "40191226",
"name": "定位",
"font_class": "dingwei",
"unicode": "e82d",
"unicode_decimal": 59437
},
{
"icon_id": "40191227",
"name": "关闭",
"font_class": "guanbi",
"unicode": "e82e",
"unicode_decimal": 59438
},
{
"icon_id": "40191228",
"name": "收藏",
"font_class": "shoucang",
"unicode": "e82f",
"unicode_decimal": 59439
},
{
"icon_id": "40191229",
"name": "时间",
"font_class": "shijian",
"unicode": "e830",
"unicode_decimal": 59440
},
{
"icon_id": "40191230",
"name": "场馆",
"font_class": "changguan",
"unicode": "e831",
"unicode_decimal": 59441
},
{
"icon_id": "40191231",
"name": "未选中",
"font_class": "weixuanzhong",
"unicode": "e832",
"unicode_decimal": 59442
},
{
"icon_id": "40191232",
"name": "选择",
"font_class": "xuanze",
"unicode": "e833",
"unicode_decimal": 59443
},
{
"icon_id": "40191233",
"name": "选择-fill",
"font_class": "xuanze-fill",
"unicode": "e834",
"unicode_decimal": 59444
},
{
"icon_id": "40191234",
"name": "选中",
"font_class": "xuanzhong",
"unicode": "e835",
"unicode_decimal": 59445
},
{
"icon_id": "40191236",
"name": "微信支付",
"font_class": "weixinzhifu",
"unicode": "e836",
"unicode_decimal": 59446
},
{
"icon_id": "40191237",
"name": "右",
"font_class": "you",
"unicode": "e837",
"unicode_decimal": 59447
},
{
"icon_id": "40191238",
"name": "联系我们",
"font_class": "lianxiwomen",
"unicode": "e838",
"unicode_decimal": 59448
},
{
"icon_id": "40191239",
"name": "余额",
"font_class": "yue",
"unicode": "e839",
"unicode_decimal": 59449
}
]
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -55,3 +55,13 @@ a {
::-webkit-scrollbar-track {
background: transparent;
}
.match-ellipsis-single {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ant-layout .ant-layout-header {
padding-inline: 15px;
}

View File

@ -1,46 +1,99 @@
import { h } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { TrophyOutlined, WalletOutlined } from '@ant-design/icons-vue';
import { routes } from '@/router/routes';
import { generateMenu, type MenuItem } from '@/router/utils';
import { useBreadcrumb } from './useBreadcrumb';
/**
* Hook
*
* @description
*
* - generateMenu meta.title / meta.icon
* - /
* - useBreadcrumb route.matched
*/
export function useBasicLayout() {
// 路由实例,用于页面跳转
const router = useRouter();
// 当前路由信息
const route = useRoute();
// 获取面包屑数据
const { breadcrumbItems } = useBreadcrumb();
const menuItems = [
{
key: '/events',
icon: h(TrophyOutlined),
label: '我的赛事',
},
// { TODO 本期先做隐藏
// key: '/wallet',
// icon: h(WalletOutlined),
// label: '钱包管理',
// },
];
/**
*
* ("/")
*/
const layoutChildren = routes.find((r) => r.path === '/' && r.children?.length)?.children ?? [];
const menuItems = generateMenu(layoutChildren);
/**
* key
* submenu selected
*/
const leafMenuKeys = new Set<string>();
const collectLeafKeys = (items: MenuItem[]) => {
items.forEach((item) => {
if (item.children?.length) {
collectLeafKeys(item.children);
} else {
leafMenuKeys.add(item.key);
}
});
};
collectLeafKeys(menuItems);
/**
* key
*
*
* 1. ->
* 2. meta.activeMenu key
* 3.
*/
const getActiveMenuKey = () => {
const matchedRoutes = route.matched;
for (const r of matchedRoutes) {
if (leafMenuKeys.has(route.path)) {
return route.path;
}
for (const r of route.matched) {
if (r.meta?.activeMenu) {
return r.meta.activeMenu as string;
}
}
if (route.path.startsWith('/events')) return '/events';
if (route.path.startsWith('/wallet')) return '/wallet';
return route.path;
};
const handleMenuClick = (info: any) => {
/**
* keys
*
*/
const getOpenKeys = () => {
return menuItems
.filter(
(item) =>
item.children?.length &&
(route.path === item.key || route.path.startsWith(`${item.key}/`)),
)
.map((item) => item.key);
};
/**
*
*/
const handleMenuClick = (info: { key: string | number }) => {
router.push(String(info.key));
};
return {
// 面包屑数据
breadcrumbItems,
// 菜单配置(由路由生成)
menuItems,
// 获取激活菜单 key
getActiveMenuKey,
// 获取展开菜单 keys
getOpenKeys,
// 菜单点击处理
handleMenuClick,
};
}

View File

@ -1,28 +1,39 @@
import { computed, h } from 'vue';
import { useRoute, useRouter } from 'vue-router';
/**
* Hook
*
* @description
* route.matched
* meta.title +
* /
*
* @returns breadcrumbs -
* @returns breadcrumbItems -
*/
export function useBreadcrumb() {
const route = useRoute();
const router = useRouter();
/**
*
* path
* 使
*/
const buildPath = (currentPath: string, recordPath: string): string => {
const buildPath = (parentPath: string, recordPath: string): string => {
if (recordPath.startsWith('/')) {
return recordPath;
}
return currentPath ? `${currentPath}/${recordPath}`.replace(/\/+/g, '/') : `/${recordPath}`;
return `${parentPath}/${recordPath}`.replace(/\/+/g, '/');
};
const breadcrumbs = computed(() => {
const matched = route.matched;
const items: Array<{ title: string; path?: string }> = [];
const items: Array<{ title: string; path: string }> = [];
let currentPath = '';
for (const record of matched) {
for (const record of route.matched) {
currentPath = buildPath(currentPath, record.path);
if (record.meta && record.meta.title) {
if (record.meta?.title) {
items.push({
title: record.meta.title as string,
path: currentPath,
@ -38,25 +49,19 @@ export function useBreadcrumb() {
key: index,
title:
item.path && index !== breadcrumbs.value.length - 1
? h(
? // 非末项:渲染为可点击链接
h(
'a',
{
onClick: (e: Event) => {
e.preventDefault();
// 最保守方案:只有当前路径包含 /bracket/ 且目标路径是 /events/bracket 时才保留 query
// 也就是只有:/events/bracket/xxx → /events/bracket 时才保留
const targetPath = item.path || '/';
const isBracketChildPage =
route.path.includes('/bracket/') && targetPath === '/events/bracket';
router.push({
path: targetPath,
query: isBracketChildPage ? route.query : {},
});
router.push(item.path);
},
},
item.title,
)
: item.title,
: // 末项:仅显示文本,表示当前所在位置
item.title,
}));
});

View File

@ -1,57 +1,200 @@
.container {
min-height: 100vh;
background: #f0f2f5;
}
.sider {
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.08);
background: #fff !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
:deep(.ant-menu) {
background: #fff;
border-right: 1px solid #f0f0f0;
}
:deep(.ant-menu-item) {
margin: 0;
border-radius: 0;
color: #595959;
&:hover {
background: #e6f7ff;
color: #1890ff;
}
}
:deep(.ant-menu-item-selected) {
position: relative;
background: linear-gradient(90deg, #e6f7ff 0%, #bae7ff 100%) !important;
color: #1890ff !important;
font-weight: 500;
// 左侧高亮指示条,标识当前选中的菜单项
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: #1890ff;
}
}
:deep(.ant-menu-submenu-title) {
margin: 0;
color: #595959;
&:hover {
color: #1890ff;
}
}
:deep(.ant-menu-submenu-selected > .ant-menu-submenu-title) {
color: #1890ff !important;
}
}
.logo {
height: 56px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 24px;
overflow: hidden;
color: #fff;
background: rgba(255, 255, 255, 0.08);
color: #262626;
background: #fff;
border-bottom: 1px solid #f0f0f0;
gap: 12px;
}
.logoIcon {
width: 32px;
height: 32px;
background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
border-radius: 8px;
flex-shrink: 0;
}
.logoText {
font-size: 16px;
font-size: 18px;
font-weight: 600;
white-space: nowrap;
color: #262626;
}
.header {
background: #fff;
padding: 0 16px;
background: #ffffff !important;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
border-bottom: 1px solid #f0f0f0;
:deep(.ant-layout-header) {
background: #ffffff !important;
}
}
.headerLeft {
display: flex;
align-items: center;
gap: 12px;
}
.headerRight {
display: flex;
align-items: center;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
}
.trigger {
font-size: 18px;
cursor: pointer;
transition: color 0.3s;
padding: 0 12px;
padding: 8px;
display: inline-flex;
align-items: center;
color: #595959;
border-radius: 4px;
&:hover {
color: #1677ff;
color: #1890ff;
background: #f5f5f5;
}
}
.content {
margin: 16px;
padding: 24px;
background: #fff;
border-radius: 8px;
min-height: 280px;
.refreshIcon {
font-size: 16px;
cursor: pointer;
padding: 8px;
display: inline-flex;
align-items: center;
color: #595959;
border-radius: 4px;
&:hover {
color: #1890ff;
background: #f5f5f5;
}
}
.footer {
text-align: center;
color: rgba(0, 0, 0, 0.45);
.breadcrumb {
margin-left: 12px;
color: #8c8c8c;
:deep(.ant-breadcrumb-link) {
color: #8c8c8c;
&:hover {
color: #1890ff;
}
}
:deep(.ant-breadcrumb-last) {
color: #262626;
}
}
.headerIcon {
font-size: 16px;
cursor: pointer;
color: #595959;
padding: 8px;
border-radius: 4px;
transition: all 0.3s;
&:hover {
color: #1890ff;
background: #f5f5f5;
}
}
.userInfo {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 6px 12px;
border-radius: 4px;
transition: background 0.3s;
// &:hover {
// background: #f5f5f5;
// }
}
.userName {
color: #262626;
font-size: 14px;
}
.content {
margin: 0;
background: #f0f2f5;
min-height: calc(100vh - 64px);
overflow: auto;
}
.pageContent {
padding: 15px;
}

View File

@ -1,69 +1,171 @@
import { computed, defineComponent, ref } from 'vue';
import { useRouter, useRoute, RouterView } from 'vue-router';
import { Layout, Menu } from 'ant-design-vue';
import type { MenuProps } from 'ant-design-vue';
import { Layout, Menu, Breadcrumb, Modal, Avatar, Space } from 'ant-design-vue';
import { RouterView, useRouter } from 'vue-router';
import {
DashboardOutlined,
InfoCircleOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
ReloadOutlined,
SearchOutlined,
BellOutlined,
FullscreenOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
import { useBasicLayout } from '../hooks/useBasicLayout';
import { auth } from '@/hooks/useAuth';
import styles from './BasicLayout.module.less';
const { Header, Sider, Content, Footer } = Layout;
// 解构 Layout 组件
const { Header, Sider, Content } = Layout;
/**
* 基础布局:左侧导航 + + +
*
*
* @description
* CPMS
* - Logo +
* - Header + +
* -
*
* @example
* ```typescript
* // 在路由中使用
* {
* path: '/',
* component: BasicLayout,
* children: [...]
* }
* ```
*/
export default defineComponent({
name: 'BasicLayout',
setup() {
const route = useRoute();
// 使用布局 Hook 获取菜单和面包屑数据
const { breadcrumbItems, menuItems, getActiveMenuKey, getOpenKeys, handleMenuClick } =
useBasicLayout();
// 路由实例,用于退出登录后跳转到登录页
const router = useRouter();
/**
*
* true:
* false: +
*/
const collapsed = ref(false);
const selectedKeys = computed<string[]>(() => [route.path]);
const menuItems: MenuProps['items'] = [
{ key: '/dashboard', icon: () => <DashboardOutlined />, label: '工作台' },
{ key: '/about', icon: () => <InfoCircleOutlined />, label: '关于' },
];
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
router.push(key as string);
};
/**
* /
*/
const toggleCollapsed = () => {
collapsed.value = !collapsed.value;
};
/**
* 退
* -
* - ->
* - ->
*/
const handleLogout = () => {
Modal.confirm({
title: '确认退出',
content: '确定要退出当前账号吗?',
okText: '确定',
cancelText: '取消',
onOk: () => {
// 清除本地登录态(不调用接口),否则路由守卫会把 /login 重定向回首页
auth.logout();
router.replace('/login');
},
});
};
return () => (
<Layout class={styles.container}>
<Sider class={styles.sider} collapsed={collapsed.value} trigger={null} collapsible>
{/* 左侧侧边栏 */}
<Sider
class={styles.sider}
collapsed={collapsed.value}
trigger={null}
collapsible
width={220}
>
{/* Logo 区域 */}
<div class={styles.logo}>
<span class={styles.logoText}>{collapsed.value ? 'CPMS' : 'CPMS 运营平台'}</span>
<div class={styles.logoIcon} />
<span class={styles.logoText}>{collapsed.value ? 'CPMS' : 'CPMS运营平台'}</span>
</div>
{/* 导航菜单 */}
<Menu
theme="dark"
theme="light"
mode="inline"
selectedKeys={selectedKeys.value}
selectedKeys={[getActiveMenuKey()]}
openKeys={getOpenKeys()}
items={menuItems}
onClick={handleMenuClick}
/>
</Sider>
{/* 右侧内容区域 */}
<Layout>
<Header class={styles.header}>
<span class={styles.trigger} onClick={toggleCollapsed}>
{collapsed.value ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</span>
{/* 顶部 Header */}
<Header class={styles.header} style={{ background: '#ffffff' }}>
<div class={styles.headerLeft}>
{/* 折叠/展开按钮 */}
<span class={styles.trigger} onClick={toggleCollapsed}>
{collapsed.value ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</span>
{/* 刷新按钮 */}
{/* <span class={styles.refreshIcon}>
<ReloadOutlined />
</span> */}
{/* 面包屑导航 */}
{breadcrumbItems.value.length > 0 && (
<Breadcrumb class={styles.breadcrumb}>
{breadcrumbItems.value.map((item) => (
<Breadcrumb.Item key={item.key}>{item.title}</Breadcrumb.Item>
))}
</Breadcrumb>
)}
</div>
{/* Header 右侧:用户信息 */}
<div class={styles.headerRight}>
<Space size="middle">
{/* 搜索图标 */}
{/* <span class={styles.headerIcon}>
<SearchOutlined />
</span> */}
{/* 通知图标 */}
{/* <span class={styles.headerIcon}>
<BellOutlined />
</span> */}
{/* 全屏图标 */}
{/* <span class={styles.headerIcon}>
<FullscreenOutlined />
</span> */}
{/* 用户信息:点击弹出退出登录确认 */}
<div class={styles.userInfo} onClick={handleLogout}>
<Avatar size="small" icon={<UserOutlined />} />
<span class={styles.userName}></span>
</div>
</Space>
</div>
</Header>
{/* 内容区域 */}
<Content class={styles.content}>
<RouterView />
<div class={styles.pageContent}>
{/* 路由视图:根据当前路由渲染对应页面 */}
<RouterView />
</div>
</Content>
<Footer class={styles.footer}>CPMS ©{new Date().getFullYear()}</Footer>
</Layout>
</Layout>
);

View File

@ -5,6 +5,7 @@ import 'ant-design-vue/dist/reset.css';
import App from './App';
import router from './router';
import './assets/styles/index.less';
import './assets/iconfont/iconfont.css';
const app = createApp(App);

View File

@ -1,9 +1,9 @@
import { createRouter, createWebHistory } from 'vue-router';
import { createRouter, createWebHashHistory } from 'vue-router';
import { routes } from './routes';
import { auth } from '@/hooks/useAuth';
const router = createRouter({
history: createWebHistory(import.meta.env.VITE_BASE_URL || '/'),
history: createWebHashHistory(import.meta.env.VITE_BASE_URL || '/'),
routes,
scrollBehavior: () => ({ left: 0, top: 0 }),
});

View File

@ -1,40 +1,23 @@
import type { RouteRecordRaw } from 'vue-router';
import {
BarChartOutlined,
UserOutlined,
StarOutlined,
FileTextOutlined,
NotificationOutlined,
BorderTopOutlined,
BoxPlotOutlined,
SafetyCertificateOutlined,
} from '@ant-design/icons-vue';
import { createRoute } from './utils';
/**
* ====== ======
* , eventsRouteswalletRoutes ,
* routes children
* ():
*
* const eventsRoutes = {
* path: '/events',
* name: 'EventsModule',
* redirect: '/events/list',
* meta: { title: '我的赛事', activeMenu: '/events' },
* children: [
* createRoute('list', () => import('@/pages/events/list'), { title: '赛事列表' }),
* {
* path: 'bracket',
* name: 'BracketPage',
* component: () => import('@/pages/events/bracket'),
* meta: { title: '赛事详情' },
* children: [
* createRoute('player-management', () => import('@/pages/events/bracket/player-management'), { title: '选手管理' }),
* createRoute('match-result', () => import('@/pages/events/bracket/match-result'), { title: '比赛结果' }),
* ],
* },
* ],
* };
*/
export const routes: RouteRecordRaw[] = [
// ── 登录页(不经过布局)──
{
path: '/login',
name: 'Login',
component: () => import('@/pages/login'),
meta: { title: '登录' },
meta: { title: '登录', hideInMenu: true },
},
// ── 主布局(需要登录)──
@ -42,17 +25,149 @@ export const routes: RouteRecordRaw[] = [
path: '/',
component: () => import('@/layouts/BasicLayout'),
children: [
// 业务模块路由在此添加,例如:eventsRoutes, walletRoutes
// 数据分析
createRoute('/dashboard', () => import('@/views/Dashboard'), {
title: '工作台',
icon: 'DashboardOutlined',
title: '数据分析',
icon: BarChartOutlined,
}),
// 场馆列表(分组)
{
path: '/venue',
name: 'Venue',
redirect: '/venue/list',
meta: {
title: '场馆列表',
icon: 'icon-changguan1',
activeMenu: '/venue',
},
children: [
createRoute('/venue/list', () => import('@/views/venue/list'), {
title: '馆客多',
}),
createRoute('/venue/records', () => import('@/views/venue/records'), {
title: '收录',
}),
],
},
// C端用户列表
createRoute('/users', () => import('@/views/Dashboard'), {
title: 'C端用户列表',
icon: UserOutlined,
}),
// 点评管理
createRoute('/reviews', () => import('@/views/Dashboard'), {
title: '点评管理',
icon: StarOutlined,
}),
// 广告位管理
createRoute('/ads', () => import('@/views/Dashboard'), {
title: '广告位管理',
icon: NotificationOutlined,
}),
// 资讯管理
createRoute('/news', () => import('@/views/Dashboard'), {
title: '资讯管理',
icon: FileTextOutlined,
}),
// 申请入驻管理
createRoute('/apply', () => import('@/views/Dashboard'), {
title: '申请入驻管理',
icon: UserOutlined,
}),
// 反馈管理
createRoute('/feedback', () => import('@/views/Dashboard'), {
title: '反馈管理',
icon: BoxPlotOutlined,
}),
// 活动管理(分组)
{
path: '/activity',
name: 'Activity',
redirect: '/activity/list',
meta: { title: '活动管理', icon: 'icon-rili', activeMenu: '/activity' },
children: [
createRoute('/activity/list', () => import('@/views/Dashboard'), {
title: '活动列表',
}),
createRoute('/activity/categories', () => import('@/views/Dashboard'), {
title: '活动分类',
}),
],
},
// 场馆设置(分组)
{
path: '/venue-settings',
name: 'VenueSettings',
redirect: '/venue-settings/basic',
meta: {
title: '场馆设置',
icon: BorderTopOutlined,
activeMenu: '/venue-settings',
},
children: [
createRoute('/venue-settings/basic', () => import('@/views/Dashboard'), {
title: '基本设置',
}),
createRoute('/venue-settings/service', () => import('@/views/Dashboard'), {
title: '服务设置',
}),
],
},
// 钱包管理(分组)
{
path: '/wallet',
name: 'Wallet',
redirect: '/wallet/records',
meta: { title: '钱包管理', icon: 'icon-qianbao', activeMenu: '/wallet' },
children: [
createRoute('/wallet/records', () => import('@/views/Dashboard'), {
title: '交易记录',
}),
createRoute('/wallet/withdraw', () => import('@/views/Dashboard'), {
title: '提现管理',
}),
createRoute('/wallet/recharge', () => import('@/views/Dashboard'), {
title: '充值管理',
}),
],
},
// 系统管理(分组)
{
path: '/system',
name: 'System',
redirect: '/system/users',
meta: {
title: '系统管理',
icon: SafetyCertificateOutlined,
activeMenu: '/system',
},
children: [
createRoute('/system/users', () => import('@/views/Dashboard'), {
title: '用户管理',
}),
createRoute('/system/roles', () => import('@/views/Dashboard'), {
title: '角色管理',
}),
createRoute('/system/permissions', () => import('@/views/Dashboard'), {
title: '权限管理',
}),
createRoute('/system/logs', () => import('@/views/Dashboard'), {
title: '操作日志',
}),
],
},
// 关于(不在侧边栏显示)
createRoute('/about', () => import('@/views/About'), {
title: '关于',
icon: 'InfoCircleOutlined',
hideInMenu: true,
}),
// 默认重定向
{ path: '/', redirect: '/dashboard' },
],

View File

@ -1,20 +1,25 @@
import { h, type Component, type VNode } from 'vue';
import type { RouteRecordRaw } from 'vue-router';
/** 路由 meta 类型 */
export type AppMeta = {
/** 页面 / 菜单标题,同时用于面包屑 */
title: string;
icon?: string;
/**
*
* - iconfont class 'icon-changguan1'
* - ant-design BarChartOutlined
*/
icon?: Component | string;
/** 是否在侧边栏菜单中隐藏(详情页等) */
hideInMenu?: boolean;
/** 高亮的菜单 key路径不等于菜单 key 的场景,如详情页) */
activeMenu?: string;
};
/**
*
* PascalCase name
*
* @example
* createRoute('list', () => import('@/pages/home'), { title: '赛事列表' })
* // => { path: 'list', name: 'List', meta: {...}, component: ... }
*/
export function createRoute(
path: string,
@ -38,3 +43,47 @@ export function createRoute(
children,
};
}
/** 侧边栏菜单项ant-design-vue Menu 的 items 结构) */
export interface MenuItem {
key: string;
label: string;
icon?: VNode;
children?: MenuItem[];
}
/**
*
*
* @description
* /
* - meta.title hideInMenu
* -
* - meta.icon
*
* @param routes children
*/
export function generateMenu(routes: RouteRecordRaw[] = []): MenuItem[] {
const items: MenuItem[] = [];
for (const route of routes) {
const meta = route.meta as AppMeta | undefined;
if (!meta?.title || meta.hideInMenu) {
continue;
}
const children = generateMenu(route.children);
const item: MenuItem = { key: route.path, label: meta.title };
if (meta.icon) {
// 字符串 -> iconfont 图标(<i class="iconfont icon-xxx" />);组件 -> 直接渲染
item.icon =
typeof meta.icon === 'string' ? h('i', { class: ['iconfont', meta.icon] }) : h(meta.icon);
}
if (children.length) {
item.children = children;
}
items.push(item);
}
return items;
}

View File

@ -27,7 +27,8 @@ export interface SelectOption {
declare module 'vue-router' {
interface RouteMeta {
title?: string;
icon?: string;
/** 菜单图标iconfont 的 class 名(字符串) 或 组件(如 antd 图标) */
icon?: import('vue').Component | string;
/** 是否需要登录 */
requiresAuth?: boolean;
/** 是否在菜单中隐藏 */

View File

@ -1,27 +0,0 @@
.container {
// 容器
}
.title {
margin-top: 0;
margin-bottom: 24px;
font-size: 20px;
font-weight: 600;
}
.panel {
margin-top: 16px;
ul {
margin: 12px 0 0;
padding-left: 20px;
line-height: 2;
}
code {
padding: 2px 8px;
background: #f5f5f5;
border-radius: 4px;
font-size: 13px;
}
}

View File

@ -1,67 +0,0 @@
import { defineComponent } from 'vue';
import { Card, Col, Row, Statistic } from 'ant-design-vue';
import styles from './Dashboard.module.less';
interface StatItem {
title: string;
value: number;
suffix: string;
precision?: number;
}
/**
* /
*/
export default defineComponent({
name: 'Dashboard',
setup() {
const stats: StatItem[] = [
{ title: '今日访问', value: 1280, suffix: '次' },
{ title: '活跃用户', value: 368, suffix: '人' },
{ title: '订单总数', value: 9920, suffix: '单' },
{ title: '处理率', value: 96.8, suffix: '%', precision: 1 },
];
return () => (
<div class={styles.container}>
<h2 class={styles.title}></h2>
<Row gutter={[16, 16]}>
{stats.map((item) => (
<Col key={item.title} xs={24} sm={12} lg={6}>
<Card>
<Statistic
title={item.title}
value={item.value}
suffix={item.suffix}
precision={item.precision}
/>
</Card>
</Col>
))}
</Row>
<Card class={styles.panel} title="项目说明">
<p>
使 CPMS Vue 3 + Vite + TypeScript + Ant Design Vue
, TSX ,
</p>
<ul>
<li>
:<code>npm run dev</code>
</li>
<li>
():<code>npm run build:test</code>
</li>
<li>
():<code>npm run build:prod</code>
</li>
<li>
:<code>npm run lint</code>
</li>
</ul>
</Card>
</div>
);
},
});

View File

@ -0,0 +1,184 @@
import { defineComponent, reactive, ref, watch } from 'vue';
import { Modal, Form, Input, Select, message } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import { saveVenue, statusMap, type VenueSaveParams } from '../model';
interface AddVenueModalProps {
open: boolean;
onClose: () => void;
onSuccess?: () => void;
}
/**
*
*/
export default defineComponent({
name: 'AddVenueModal',
props: {
open: {
type: Boolean,
default: false,
},
onClose: {
type: Function,
required: true,
},
onSuccess: {
type: Function,
default: undefined,
},
},
setup(props: AddVenueModalProps) {
const labelCol = { span: 5 };
const wrapperCol = { span: 18 };
// 表单实例
const formRef = ref();
// 提交中状态
const submitting = ref(false);
// 表单数据初始值
const getInitialForm = (): VenueSaveParams => ({
name: '',
type: '',
contact: '',
phone: '',
address: '',
status: 'active',
});
const formState = reactive<VenueSaveParams>(getInitialForm());
// 校验规则
const rules: Record<string, Rule[]> = {
name: [{ required: true, message: '请输入场馆名称', trigger: 'blur' }],
type: [{ required: true, message: '请输入场馆类型', trigger: 'blur' }],
contact: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
phone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{
pattern: /^1[3-9]\d{9}$/,
message: '手机号格式不正确',
trigger: 'blur',
},
],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
};
// 打开时重置表单
watch(
() => props.open,
(open) => {
if (open) {
Object.assign(formState, getInitialForm());
formRef.value?.clearValidate?.();
}
},
);
/** 确定:校验 -> 提交 -> 成功回调 */
const handleOk = async () => {
try {
await formRef.value?.validate();
} catch {
// 校验不通过,直接返回
return;
}
submitting.value = true;
try {
await saveVenue({ ...formState });
message.success('新增成功');
props.onSuccess?.();
props.onClose();
} catch {
message.error('新增失败,请重试');
} finally {
submitting.value = false;
}
};
return () => (
<Modal
open={props.open}
title="新增场馆"
width={560}
centered
destroyOnClose
confirmLoading={submitting.value}
onOk={handleOk}
onCancel={() => props.onClose()}
>
<Form
ref={formRef}
model={formState}
rules={rules}
labelCol={labelCol}
wrapperCol={wrapperCol}
style={{ marginTop: '8px' }}
>
<Form.Item label="场馆名称" name="name">
<Input
value={formState.name}
onChange={(e: any) => {
formState.name = e.target.value;
}}
placeholder="请输入场馆名称"
/>
</Form.Item>
<Form.Item label="场馆类型" name="type">
<Input
value={formState.type}
onChange={(e: any) => {
formState.type = e.target.value;
}}
placeholder="请输入场馆类型"
/>
</Form.Item>
<Form.Item label="联系人" name="contact">
<Input
value={formState.contact}
onChange={(e: any) => {
formState.contact = e.target.value;
}}
placeholder="请输入联系人"
/>
</Form.Item>
<Form.Item label="联系电话" name="phone">
<Input
value={formState.phone}
onChange={(e: any) => {
formState.phone = e.target.value;
}}
placeholder="请输入联系电话"
/>
</Form.Item>
<Form.Item label="状态" name="status">
<Select
value={formState.status}
onChange={(val: any) => {
formState.status = val;
}}
placeholder="请选择状态"
>
{Object.entries(statusMap).map(([value, { text }]) => (
<Select.Option key={value} value={value}>
{text}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="地址" name="address">
<Input.TextArea
value={formState.address}
onChange={(e: any) => {
formState.address = e.target.value;
}}
placeholder="请输入地址"
rows={3}
/>
</Form.Item>
</Form>
</Modal>
);
},
});

View File

@ -0,0 +1,86 @@
.container {
display: flex;
flex-direction: column;
height: calc(100vh - 64px - 30px);
}
.filterCard {
flex-shrink: 0;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
margin-bottom: 16px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
:global(.ant-card-body) {
padding: 20px 24px;
}
}
.filterRow {
:global(.ant-form) {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
row-gap: 12px;
column-gap: 16px;
}
:global(.ant-form-item) {
flex-shrink: 0;
margin-right: 0;
margin-bottom: 0;
}
}
.tableCard {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
:global(.ant-card-body) {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
padding: 0;
}
}
.tableBody {
flex: 1;
min-height: 0;
overflow: auto;
}
.tableHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
}
.tableFooter {
flex-shrink: 0;
padding: 16px 24px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
}
.pagination {
display: flex;
align-items: center;
gap: 16px;
}
.paginationText {
color: #595959;
font-size: 14px;
}

View File

@ -0,0 +1,247 @@
import { defineComponent } from 'vue';
import {
Card,
Table,
Input,
Select,
Button,
Space,
Tag,
Popconfirm,
Tooltip,
Pagination,
Form,
} from 'ant-design-vue';
import {
SearchOutlined,
EditOutlined,
DeleteOutlined,
PlusOutlined,
EyeOutlined,
} from '@ant-design/icons-vue';
import { useDashboardModel, type VenueItem } from './model';
import AddVenueModal from './components/AddVenueModal';
import styles from './index.module.less';
/**
* /
*/
export default defineComponent({
name: 'Dashboard',
setup() {
const {
tableData,
currentPage,
formRef,
formState,
statusMap,
addModalOpen,
openAddModal,
closeAddModal,
handleReset,
fetchVenueList,
} = useDashboardModel();
const columns = [
{
title: '序号',
dataIndex: 'id',
key: 'id',
width: 100,
},
{
title: '场馆名称',
dataIndex: 'name',
key: 'name',
width: 150,
},
{
title: '场馆类型',
dataIndex: 'type',
key: 'type',
width: 120,
},
{
title: '联系人',
dataIndex: 'contact',
key: 'contact',
width: 120,
},
{
title: '联系电话',
dataIndex: 'phone',
key: 'phone',
width: 140,
},
{
title: '地址',
dataIndex: 'address',
key: 'address',
// 开启文本溢出省略 + 悬浮 tooltip
customRender: ({ record }: { record: VenueItem }) => (
<Tooltip title={record.address}>
<div class="match-ellipsis-single">{record.address}</div>
</Tooltip>
),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
customRender: ({ record }: { record: VenueItem }) => {
const status = statusMap[record.status] || {
color: 'default',
text: '未知',
};
return <Tag color={status.color}>{status.text}</Tag>;
},
},
{
title: '',
key: 'avatar',
width: 80,
},
{
title: '操作',
key: 'action',
width: 230,
fixed: 'right' as const,
customRender: () => (
<Space size="small">
<Button type="link" size="small" icon={<EyeOutlined />}>
</Button>
{/* <i class="iconfont icon-fenzutitle" style={{ fontSize: '18px', color: '#1677ff' }} /> */}
<Button type="link" size="small" icon={<EditOutlined />}>
</Button>
<Popconfirm title="确定要删除吗?" okText="确定" cancelText="取消">
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</Space>
),
},
];
return () => (
<div class={styles.container}>
<Card class={styles.filterCard}>
<div class={styles.filterRow}>
<Form ref={formRef} layout="inline">
<Form.Item name="name">
<Input
value={formState.name}
onChange={(e: any) => {
formState.name = e.target.value;
}}
placeholder="请输入场馆名称"
prefix={<SearchOutlined />}
style={{ width: '200px' }}
/>
</Form.Item>
<Form.Item label="场馆类型" name="type">
<Select
value={formState.type}
onChange={(val: any) => {
formState.type = val;
}}
placeholder="请选择场馆类型"
style={{ width: '160px' }}
allowClear={false}
/>
</Form.Item>
<Form.Item label="状态" name="status">
<Select
value={formState.status}
onChange={(val: any) => {
formState.status = val;
}}
placeholder="请选择状态"
style={{ width: '140px' }}
allowClear={false}
>
{Object.entries(statusMap).map(([value, { text }]) => (
<Select.Option key={value} value={value}>
{text}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="联系人" name="contact">
<Input
value={formState.contact}
onChange={(e: any) => {
formState.contact = e.target.value;
}}
placeholder="请输入联系人"
style={{ width: '150px' }}
/>
</Form.Item>
<Form.Item label="联系电话" name="phone">
<Input
value={formState.phone}
onChange={(e: any) => {
formState.phone = e.target.value;
}}
placeholder="请输入联系电话"
style={{ width: '150px' }}
/>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary"></Button>
<Button onClick={handleReset}></Button>
</Space>
</Form.Item>
</Form>
</div>
</Card>
<Card class={styles.tableCard}>
<div class={styles.tableHeader}>
<span>{/* 场馆列表 */}</span>
<Button
type="primary"
/* icon={<i class="iconfont icon-tianjia" />} */
icon={<PlusOutlined />}
onClick={openAddModal}
>
</Button>
</div>
<div class={styles.tableBody}>
<Table
columns={columns}
dataSource={tableData.value}
pagination={false}
scroll={{ x: 1400 }}
/>
</div>
<div class={styles.tableFooter}>
<Pagination
current={currentPage.value}
onChange={(page: number) => {
currentPage.value = page;
}}
total={500}
showSizeChanger
showQuickJumper
showTotal={(total: number) => `${total}`}
/>
</div>
</Card>
{/* 新增场馆弹框 */}
<AddVenueModal
open={addModalOpen.value}
onClose={closeAddModal}
onSuccess={fetchVenueList}
/>
</div>
);
},
});

View File

@ -0,0 +1,2 @@
export * from './useDashboardModel';
export * from './services';

View File

@ -0,0 +1,119 @@
import { get, post } from '@/utils/request';
/** 场馆列表行数据 */
export interface VenueItem {
key: string;
id: string;
name: string;
type: string;
contact: string;
status: string;
phone: string;
address: string;
createTime: string;
}
/** 场馆状态映射:状态值 -> 颜色 / 文案 */
export const statusMap: Record<string, { color: string; text: string }> = {
active: { color: 'green', text: '正常' },
inactive: { color: 'red', text: '停用' },
pending: { color: 'orange', text: '待审' },
};
/** 场馆列表查询参数 */
export interface VenueListParams {
page: number;
size: number;
name?: string;
type?: string;
status?: string;
contact?: string;
phone?: string;
}
const venueListApi = '/venue/page';
/**
*
* @description useDashboardModel mock
*/
export const getVenueList = (params: VenueListParams) => {
return get(venueListApi, params);
};
/** 新增场馆参数 */
export interface VenueSaveParams {
name: string;
type: string;
contact: string;
phone: string;
address: string;
status: string;
}
const venueSaveApi = '/venue/save';
/** 新增场馆接口 */
export const saveVenue = (params: VenueSaveParams) => {
return post(venueSaveApi, params);
};
/** 临时 mock 数据(接口对接前占位展示) */
export const mockVenueList: VenueItem[] = [
{
key: '1',
id: 'V001',
name: '健身中心',
type: '健身馆',
contact: '张三',
status: 'active',
phone: '13999999999',
address: '北京市朝阳区xxx路xxx号',
createTime: '2024-01-15 10:30:00',
},
{
key: '2',
id: 'V002',
name: '篮球公园',
type: '篮球馆',
contact: '李四',
status: 'active',
phone: '13999999999',
address: '上海市浦东新区xxx路xxx号',
createTime: '2024-01-16 14:20:00',
},
{
key: '3',
id: 'V003',
name: '游泳馆',
type: '游泳馆',
contact: '王五',
status: 'inactive',
phone: '13999999999',
address: '广州市天河区xxx路xxx号',
createTime: '2024-01-17 09:15:00',
},
{
key: '4',
id: 'V004',
name: '羽毛球馆',
type: '羽毛球馆',
contact: '马六',
status: 'active',
phone: '13999999999',
address: '深圳市南山区xxx路xxx号',
createTime: '2024-01-18 16:45:00',
},
{
key: '5',
id: 'V005',
name: '网球中心',
type: '网球馆',
contact: '冯七',
status: 'pending',
phone: '13999999999',
address:
'杭州市西湖区xxx路xxx号杭州市西湖区xxx路xxx号杭州市西湖区xxx路xxx号杭州市西湖区xxx路xxx号11',
createTime: '2024-01-19 11:00:00',
},
];

View File

@ -0,0 +1,83 @@
import { reactive, ref } from 'vue';
import { getVenueList, mockVenueList, statusMap, type VenueItem } from './services';
/** 筛选表单字段 */
export interface VenueFilterForm {
name: string;
type?: string;
status?: string;
contact: string;
phone: string;
}
/**
* Dashboard()
*
* @description
* index.tsx
*/
export function useDashboardModel() {
// 表格数据(暂用 mock接口就绪后由 fetchVenueList 填充)
const tableData = ref<VenueItem[]>(mockVenueList);
// 当前页码
const currentPage = ref(1);
// 筛选表单实例
const formRef = ref();
// 筛选表单数据
const formState = reactive<VenueFilterForm>({
name: '',
type: undefined,
status: undefined,
contact: '',
phone: '',
});
// 新增场馆弹框显隐
const addModalOpen = ref(false);
/** 打开新增弹框 */
const openAddModal = () => {
addModalOpen.value = true;
};
/** 关闭新增弹框 */
const closeAddModal = () => {
addModalOpen.value = false;
};
/** 重置筛选表单 */
const handleReset = () => {
formRef.value?.resetFields();
Object.assign(formState, {
name: '',
type: undefined,
status: undefined,
contact: '',
phone: '',
});
};
/**
*
* @description mock
*/
const fetchVenueList = async () => {
const res: any = await getVenueList({
page: currentPage.value,
size: 10,
...formState,
});
tableData.value = res?.data?.list ?? [];
};
return {
tableData,
currentPage,
formRef,
formState,
statusMap,
addModalOpen,
openAddModal,
closeAddModal,
handleReset,
fetchVenueList,
};
}

View File

@ -0,0 +1,67 @@
.container {
display: flex;
flex-direction: column;
height: calc(100vh - 64px - 30px);
}
.filterCard {
flex-shrink: 0;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
margin-bottom: 16px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
:global(.ant-card-body) {
padding: 20px 24px;
}
}
.filterRow {
:global(.ant-form) {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
row-gap: 12px;
column-gap: 16px;
}
:global(.ant-form-item) {
flex-shrink: 0;
margin-right: 0;
margin-bottom: 0;
}
}
.tableCard {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
:global(.ant-card-body) {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
padding: 0;
}
}
.tableBody {
flex: 1;
min-height: 0;
overflow: auto;
}
.tableFooter {
flex-shrink: 0;
padding: 16px 24px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
}

View File

@ -0,0 +1,139 @@
import { defineComponent } from 'vue';
import {
Card,
Table,
Input,
Select,
Button,
Space,
Tag,
Popconfirm,
Pagination,
Form,
} from 'ant-design-vue';
import { SearchOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons-vue';
import { useVenueListModel, type VenueListItem } from './model';
import styles from './index.module.less';
/**
* /
*/
export default defineComponent({
name: 'VenueList',
setup() {
const { tableData, currentPage, formRef, formState, statusMap, handleReset } =
useVenueListModel();
const columns = [
{ title: '序号', dataIndex: 'id', key: 'id', width: 100 },
{ title: '场馆名称', dataIndex: 'name', key: 'name', width: 160 },
{ title: '场馆类型', dataIndex: 'type', key: 'type', width: 140 },
{ title: '联系人', dataIndex: 'contact', key: 'contact', width: 120 },
{ title: '联系电话', dataIndex: 'phone', key: 'phone', width: 160 },
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
customRender: ({ record }: { record: VenueListItem }) => {
const status = statusMap[record.status] || {
color: 'default',
text: '未知',
};
return <Tag color={status.color}>{status.text}</Tag>;
},
},
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 },
{
title: '操作',
key: 'action',
width: 230,
fixed: 'right' as const,
customRender: () => (
<Space size="small">
<Button type="link" size="small" icon={<EyeOutlined />}>
</Button>
<Button type="link" size="small" icon={<EditOutlined />}>
</Button>
<Popconfirm title="确定要删除吗?" okText="确定" cancelText="取消">
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</Space>
),
},
];
return () => (
<div class={styles.container}>
<Card class={styles.filterCard}>
<div class={styles.filterRow}>
<Form ref={formRef} layout="inline">
<Form.Item name="name">
<Input
value={formState.name}
onChange={(e: any) => {
formState.name = e.target.value;
}}
placeholder="请输入场馆名称"
prefix={<SearchOutlined />}
style={{ width: '200px' }}
/>
</Form.Item>
<Form.Item label="状态" name="status">
<Select
value={formState.status}
onChange={(val: any) => {
formState.status = val;
}}
placeholder="请选择状态"
style={{ width: '140px' }}
allowClear={false}
>
{Object.entries(statusMap).map(([value, { text }]) => (
<Select.Option key={value} value={value}>
{text}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary"></Button>
<Button onClick={handleReset}></Button>
</Space>
</Form.Item>
</Form>
</div>
</Card>
<Card class={styles.tableCard}>
<div class={styles.tableBody}>
<Table
columns={columns}
dataSource={tableData.value}
pagination={false}
scroll={{ x: 1200 }}
/>
</div>
<div class={styles.tableFooter}>
<Pagination
current={currentPage.value}
onChange={(page: number) => {
currentPage.value = page;
}}
total={500}
showSizeChanger
showQuickJumper
showTotal={(total: number) => `${total}`}
/>
</div>
</Card>
</div>
);
},
});

View File

@ -0,0 +1,2 @@
export * from './useVenueListModel';
export * from './services';

View File

@ -0,0 +1,72 @@
import { get } from '@/utils/request';
/** 场馆列表行数据 */
export interface VenueListItem {
key: string;
id: string;
name: string;
type: string;
contact: string;
status: string;
phone: string;
createTime: string;
}
/** 场馆状态映射:状态值 -> 颜色 / 文案 */
export const statusMap: Record<string, { color: string; text: string }> = {
active: { color: 'green', text: '正常' },
inactive: { color: 'red', text: '停用' },
pending: { color: 'orange', text: '待审' },
};
/** 场馆列表查询参数 */
export interface VenueListParams {
page: number;
size: number;
name?: string;
status?: string;
}
const venueListApi = '/venue/list';
/**
* -
* @description useVenueListModel mock
*/
export const getVenueList = (params: VenueListParams) => {
return get(venueListApi, params);
};
/** 临时 mock 数据(接口对接前占位展示) */
export const mockVenueList: VenueListItem[] = [
{
key: '1',
id: 'V001',
name: '健身中心',
type: '健身馆',
contact: '张三',
status: 'active',
phone: '13999999999',
createTime: '2024-01-15 10:30:00',
},
{
key: '2',
id: 'V002',
name: '篮球公园',
type: '篮球馆',
contact: '李四',
status: 'active',
phone: '13999999999',
createTime: '2024-01-16 14:20:00',
},
{
key: '3',
id: 'V003',
name: '游泳馆',
type: '游泳馆',
contact: '王五',
status: 'inactive',
phone: '13999999999',
createTime: '2024-01-17 09:15:00',
},
];

View File

@ -0,0 +1,54 @@
import { reactive, ref } from 'vue';
import { getVenueList, mockVenueList, statusMap, type VenueListItem } from './services';
/** 筛选表单字段 */
export interface VenueListFilterForm {
name: string;
status?: string;
}
/**
* ()
*/
export function useVenueListModel() {
// 表格数据(暂用 mock接口就绪后由 fetchList 填充)
const tableData = ref<VenueListItem[]>(mockVenueList);
// 当前页码
const currentPage = ref(1);
// 筛选表单实例
const formRef = ref();
// 筛选表单数据
const formState = reactive<VenueListFilterForm>({
name: '',
status: undefined,
});
/** 重置筛选表单 */
const handleReset = () => {
formRef.value?.resetFields();
Object.assign(formState, { name: '', status: undefined });
};
/**
*
* @description mock
*/
const fetchList = async () => {
const res: any = await getVenueList({
page: currentPage.value,
size: 10,
...formState,
});
tableData.value = res?.data?.list ?? [];
};
return {
tableData,
currentPage,
formRef,
formState,
statusMap,
handleReset,
fetchList,
};
}

View File

@ -0,0 +1,67 @@
.container {
display: flex;
flex-direction: column;
height: calc(100vh - 64px - 30px);
}
.filterCard {
flex-shrink: 0;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
margin-bottom: 16px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
:global(.ant-card-body) {
padding: 20px 24px;
}
}
.filterRow {
:global(.ant-form) {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
row-gap: 12px;
column-gap: 16px;
}
:global(.ant-form-item) {
flex-shrink: 0;
margin-right: 0;
margin-bottom: 0;
}
}
.tableCard {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
:global(.ant-card-body) {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
padding: 0;
}
}
.tableBody {
flex: 1;
min-height: 0;
overflow: auto;
}
.tableFooter {
flex-shrink: 0;
padding: 16px 24px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
}

View File

@ -0,0 +1,137 @@
import { defineComponent } from 'vue';
import {
Card,
Table,
Input,
Select,
Button,
Space,
Tag,
Popconfirm,
Pagination,
Form,
} from 'ant-design-vue';
import { SearchOutlined, EyeOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { useVenueRecordsModel, type VenueRecordItem } from './model';
import styles from './index.module.less';
/**
* /
*/
export default defineComponent({
name: 'VenueRecords',
setup() {
const { tableData, currentPage, formRef, formState, statusMap, handleReset } =
useVenueRecordsModel();
const columns = [
{ title: '序号', dataIndex: 'id', key: 'id', width: 100 },
{ title: '场馆名称', dataIndex: 'venueName', key: 'venueName', width: 180 },
{ title: '来源', dataIndex: 'source', key: 'source', width: 140 },
{
title: '收录状态',
dataIndex: 'status',
key: 'status',
width: 120,
customRender: ({ record }: { record: VenueRecordItem }) => {
const status = statusMap[record.status] || {
color: 'default',
text: '未知',
};
return <Tag color={status.color}>{status.text}</Tag>;
},
},
{ title: '收录时间', dataIndex: 'recordTime', key: 'recordTime', width: 180 },
{
title: '操作',
key: 'action',
width: 240,
fixed: 'right' as const,
customRender: () => (
<Space size="small">
<Button type="link" size="small" icon={<EyeOutlined />}>
</Button>
<Button type="link" size="small" icon={<CheckOutlined />}>
</Button>
<Popconfirm title="确定要驳回吗?" okText="确定" cancelText="取消">
<Button type="link" size="small" danger icon={<CloseOutlined />}>
</Button>
</Popconfirm>
</Space>
),
},
];
return () => (
<div class={styles.container}>
<Card class={styles.filterCard}>
<div class={styles.filterRow}>
<Form ref={formRef} layout="inline">
<Form.Item name="venueName">
<Input
value={formState.venueName}
onChange={(e: any) => {
formState.venueName = e.target.value;
}}
placeholder="请输入场馆名称"
prefix={<SearchOutlined />}
style={{ width: '200px' }}
/>
</Form.Item>
<Form.Item label="收录状态" name="status">
<Select
value={formState.status}
onChange={(val: any) => {
formState.status = val;
}}
placeholder="请选择状态"
style={{ width: '140px' }}
allowClear={false}
>
{Object.entries(statusMap).map(([value, { text }]) => (
<Select.Option key={value} value={value}>
{text}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary"></Button>
<Button onClick={handleReset}></Button>
</Space>
</Form.Item>
</Form>
</div>
</Card>
<Card class={styles.tableCard}>
<div class={styles.tableBody}>
<Table
columns={columns}
dataSource={tableData.value}
pagination={false}
scroll={{ x: 1000 }}
/>
</div>
<div class={styles.tableFooter}>
<Pagination
current={currentPage.value}
onChange={(page: number) => {
currentPage.value = page;
}}
total={500}
showSizeChanger
showQuickJumper
showTotal={(total: number) => `${total}`}
/>
</div>
</Card>
</div>
);
},
});

View File

@ -0,0 +1,2 @@
export * from './useVenueRecordsModel';
export * from './services';

View File

@ -0,0 +1,64 @@
import { get } from '@/utils/request';
/** 收录记录行数据 */
export interface VenueRecordItem {
key: string;
id: string;
venueName: string;
source: string;
status: string;
recordTime: string;
}
/** 收录状态映射:状态值 -> 颜色 / 文案 */
export const statusMap: Record<string, { color: string; text: string }> = {
recorded: { color: 'green', text: '已收录' },
reviewing: { color: 'orange', text: '审核中' },
rejected: { color: 'red', text: '已驳回' },
};
/** 收录记录查询参数 */
export interface VenueRecordParams {
page: number;
size: number;
venueName?: string;
status?: string;
}
const venueRecordApi = '/venue/records';
/**
*
* @description useVenueRecordsModel mock
*/
export const getVenueRecords = (params: VenueRecordParams) => {
return get(venueRecordApi, params);
};
/** 临时 mock 数据(接口对接前占位展示) */
export const mockVenueRecords: VenueRecordItem[] = [
{
key: '1',
id: 'R001',
venueName: '健身中心',
source: '用户提交',
status: 'recorded',
recordTime: '2024-01-15 10:30:00',
},
{
key: '2',
id: 'R002',
venueName: '篮球公园',
source: '平台采集',
status: 'reviewing',
recordTime: '2024-01-16 14:20:00',
},
{
key: '3',
id: 'R003',
venueName: '游泳馆',
source: '用户提交',
status: 'rejected',
recordTime: '2024-01-17 09:15:00',
},
];

View File

@ -0,0 +1,54 @@
import { reactive, ref } from 'vue';
import { getVenueRecords, mockVenueRecords, statusMap, type VenueRecordItem } from './services';
/** 筛选表单字段 */
export interface VenueRecordFilterForm {
venueName: string;
status?: string;
}
/**
*
*/
export function useVenueRecordsModel() {
// 表格数据(暂用 mock接口就绪后由 fetchList 填充)
const tableData = ref<VenueRecordItem[]>(mockVenueRecords);
// 当前页码
const currentPage = ref(1);
// 筛选表单实例
const formRef = ref();
// 筛选表单数据
const formState = reactive<VenueRecordFilterForm>({
venueName: '',
status: undefined,
});
/** 重置筛选表单 */
const handleReset = () => {
formRef.value?.resetFields();
Object.assign(formState, { venueName: '', status: undefined });
};
/**
*
* @description mock
*/
const fetchList = async () => {
const res: any = await getVenueRecords({
page: currentPage.value,
size: 10,
...formState,
});
tableData.value = res?.data?.list ?? [];
};
return {
tableData,
currentPage,
formRef,
formState,
statusMap,
handleReset,
fetchList,
};
}

View File

@ -38,6 +38,7 @@ export default defineConfig(({ mode }) => {
},
},
build: {
target: 'es2020',
outDir: 'dist',
sourcemap: mode !== 'production',
chunkSizeWarningLimit: 1500,
@ -50,5 +51,10 @@ export default defineConfig(({ mode }) => {
},
},
},
optimizeDeps: {
esbuildOptions: {
target: 'es2020',
},
},
};
});