搭建后台框架
This commit is contained in:
parent
142150c907
commit
1f6670ad34
@ -17,6 +17,7 @@ import org.springframework.http.converter.StringHttpMessageConverter;
|
|||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||||
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
@ -35,6 +36,15 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
@Resource
|
@Resource
|
||||||
private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;
|
private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
|
registry.addMapping("/**")
|
||||||
|
.allowedOriginPatterns("*")
|
||||||
|
.allowCredentials(true)
|
||||||
|
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||||
|
.maxAge(3600);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
registry.addInterceptor(authorizationInterceptor).addPathPatterns("/api/**");
|
registry.addInterceptor(authorizationInterceptor).addPathPatterns("/api/**");
|
||||||
|
@ -14,6 +14,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@RestController
|
@RestController
|
||||||
|
@CrossOrigin
|
||||||
@RequestMapping("/api/users")
|
@RequestMapping("/api/users")
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
|
@ -49,6 +49,10 @@ public class UserServiceImpl extends ServiceImpl<UserDao, UserEntity> implements
|
|||||||
UserEntity user = getByUsername(dto.getUsername());
|
UserEntity user = getByUsername(dto.getUsername());
|
||||||
AssertUtils.isNull(user, "用户名不存在~");
|
AssertUtils.isNull(user, "用户名不存在~");
|
||||||
|
|
||||||
|
if (!dto.getRole().equals(user.getRole())){
|
||||||
|
throw new RenException("该角色没有账号");
|
||||||
|
}
|
||||||
|
|
||||||
//密码错误
|
//密码错误
|
||||||
if (!user.getPassword().equals(DigestUtil.sha256Hex(dto.getPassword()))) {
|
if (!user.getPassword().equals(DigestUtil.sha256Hex(dto.getPassword()))) {
|
||||||
throw new RenException("密码输入错误~");
|
throw new RenException("密码输入错误~");
|
||||||
|
14
src/main/resources/mapper/TokenDao.xml
Normal file
14
src/main/resources/mapper/TokenDao.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<mapper namespace="io.dao.TokenDao">
|
||||||
|
|
||||||
|
<select id="getByToken" resultType="io.entity.TokenEntity">
|
||||||
|
select * from tb_token where token = #{value}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="getByUserId" resultType="io.entity.TokenEntity">
|
||||||
|
select * from tb_token where user_id = #{value}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
44
ui/.devcontainer/devcontainer.json
Normal file
44
ui/.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
|
||||||
|
{
|
||||||
|
"name": "Node.js & TypeScript",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-18-buster",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"antfu.goto-alias",
|
||||||
|
"mikestead.dotenv",
|
||||||
|
"redhat.vscode-yaml",
|
||||||
|
"Vue.volar",
|
||||||
|
"steoates.autoimport",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"usernamehw.errorlens",
|
||||||
|
"shd101wyy.markdown-preview-enhanced",
|
||||||
|
"voorjaar.windicss-intellisense",
|
||||||
|
"yoavbls.pretty-ts-errors",
|
||||||
|
"bodil.prettier-toml",
|
||||||
|
"bungcip.better-toml"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "yarn install",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
23
ui/.editorConfig
Normal file
23
ui/.editorConfig
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
# 匹配全部文件
|
||||||
|
[*]
|
||||||
|
# 设置字符集
|
||||||
|
charset = utf-8
|
||||||
|
# 缩进风格,可选 space、tab
|
||||||
|
indent_style = tab
|
||||||
|
# 缩进的空格数,当 indent_style = tab 将使用 tab_width
|
||||||
|
# 否则使用 indent_size
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
# 结尾换行符,可选 lf、cr、crlf
|
||||||
|
end_of_line = crlf
|
||||||
|
# 在文件结尾插入新行
|
||||||
|
insert_final_newline = true
|
||||||
|
# 删除一行中的前后空格
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# 匹配 md 结尾的文件
|
||||||
|
[*.md]
|
||||||
|
insert_final_newline = false
|
||||||
|
trim_trailing_whitespace = false
|
29
ui/.env
Normal file
29
ui/.env
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# 通用环境变量
|
||||||
|
# 前端接口
|
||||||
|
VITE_API_FRONT_BASE_URL = http://localhost:18080
|
||||||
|
#后端接口
|
||||||
|
VITE_ADMIN_API_BASE_URL = http://localhost:18080
|
||||||
|
|
||||||
|
VITE_LOGIN_BG = "/login/e36341619bf8f04dcbdc6b01105a85a.png"
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
VITE_APP_TITLE = 屋屋换住系统
|
||||||
|
|
||||||
|
# markdown 渲染支持
|
||||||
|
VITE_APP_MARKDOWN = true
|
||||||
|
|
||||||
|
# 开发时的开发面板
|
||||||
|
VITE_APP_DEV_TOOLS = false
|
||||||
|
|
||||||
|
# 生产时 mock 支持
|
||||||
|
VITE_APP_MOCK_IN_PRODUCTION = false
|
||||||
|
|
||||||
|
# 生产时压缩算法,可选 gzip, brotliCompress, deflate, deflateRaw
|
||||||
|
VITE_APP_COMPRESSINON_ALGORITHM = gzip
|
||||||
|
|
||||||
|
# api 自动按需引入
|
||||||
|
# 注意设置关闭时,其他的 api 自动按需引入也将自动关闭
|
||||||
|
VITE_APP_API_AUTO_IMPORT = true
|
||||||
|
|
||||||
|
# 项目级 api 自动按需导入
|
||||||
|
VITE_APP_DIR_API_AUTO_IMPORT = true
|
4
ui/.eslintignore
Normal file
4
ui/.eslintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# 忽略 eslint 检查
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
presets/types
|
12
ui/.gitignore
vendored
Normal file
12
ui/.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
dist
|
||||||
|
.nitro
|
||||||
|
.output
|
||||||
|
env.d.ts
|
||||||
|
node_modules
|
||||||
|
.eslintcache
|
||||||
|
components.d.ts
|
||||||
|
type-router.d.ts
|
||||||
|
auto-imports.d.ts
|
||||||
|
.eslintrc-auto-import.json
|
||||||
|
vite.config.ts.timestamp*
|
||||||
|
.idea/
|
3
ui/.prettierignore
Normal file
3
ui/.prettierignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
presets/types
|
5
ui/.prettierrc.json
Normal file
5
ui/.prettierrc.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
13
ui/index.html
Normal file
13
ui/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
10
ui/locales/English/common.yml
Normal file
10
ui/locales/English/common.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# English
|
||||||
|
|
||||||
|
home: home
|
||||||
|
about: about
|
||||||
|
echarts: echarts
|
||||||
|
edit: Edit
|
||||||
|
to test HMR: to test HMR
|
||||||
|
check out: Check out
|
||||||
|
The total number of views is: The total number of views is
|
||||||
|
the official Tov + Vue + Vite template: the official Tov + Vue + Vite template
|
3
ui/locales/English/test.yml
Normal file
3
ui/locales/English/test.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# English
|
||||||
|
|
||||||
|
test.module: lanugae module test
|
10
ui/locales/简体中文/common.yml
Normal file
10
ui/locales/简体中文/common.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# 中文
|
||||||
|
|
||||||
|
home: 主页
|
||||||
|
about: 关于
|
||||||
|
echarts: 图表
|
||||||
|
edit: 编辑
|
||||||
|
to test HMR: 测试热更新
|
||||||
|
check out: 查看
|
||||||
|
The total number of views is: 总浏览数
|
||||||
|
the official Tov + Vue + Vite template: 公共的 Tov + Vue + Vite 模板
|
3
ui/locales/简体中文/test.yml
Normal file
3
ui/locales/简体中文/test.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# 简体中文
|
||||||
|
|
||||||
|
test.module: 多语言多模块测试
|
4
ui/netlify.toml
Normal file
4
ui/netlify.toml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[[redirects]]
|
||||||
|
to = "/index.html"
|
||||||
|
from = "/*"
|
||||||
|
status = 200
|
119
ui/package.json
Normal file
119
ui/package.json
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
{
|
||||||
|
"name": "后台",
|
||||||
|
"version": "1.19.0",
|
||||||
|
"description": "后台",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.12.2"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@8.15.8",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/ityped": "^1.0.3",
|
||||||
|
"@types/node": "^20.12.7",
|
||||||
|
"@typescript-eslint/parser": "7.8.0",
|
||||||
|
"@unocss/eslint-config": "0.59.4",
|
||||||
|
"@unocss/reset": "^0.59.4",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||||
|
"@vueuse/components": "^10.9.0",
|
||||||
|
"@vueuse/core": "^10.9.0",
|
||||||
|
"@vueuse/integrations": "^10.9.0",
|
||||||
|
"axios": "^1.6.8",
|
||||||
|
"browserslist": "^4.23.0",
|
||||||
|
"c8": "^9.1.0",
|
||||||
|
"changelogen": "^0.5.5",
|
||||||
|
"consola": "^3.2.3",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"defu": "^6.1.4",
|
||||||
|
"echarts": "^5.5.0",
|
||||||
|
"eslint": "8.57.0",
|
||||||
|
"eslint-config-prettier": "9.1.0",
|
||||||
|
"eslint-plugin-prettier": "5.1.3",
|
||||||
|
"eslint-plugin-vue": "^9.25.0",
|
||||||
|
"fs-extra": "^11.2.0",
|
||||||
|
"husky": "^9.0.11",
|
||||||
|
"ityped": "^1.0.3",
|
||||||
|
"kolorist": "^1.8.0",
|
||||||
|
"lightningcss": "^1.24.1",
|
||||||
|
"lint-staged": "^15.2.2",
|
||||||
|
"local-pkg": "^0.5.0",
|
||||||
|
"mockjs": "^1.1.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"perfect-debounce": "^1.0.0",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"pinia-plugin-persistedstate": "^3.2.1",
|
||||||
|
"plop": "^4.0.1",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"prism-theme-vars": "^0.2.5",
|
||||||
|
"simple-git": "^3.24.0",
|
||||||
|
"taze": "^0.13.7",
|
||||||
|
"terser": "^5.31.0",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"unocss": "^0.59.4",
|
||||||
|
"unplugin-auto-import": "^0.17.5",
|
||||||
|
"unplugin-vue-components": "^0.26.0",
|
||||||
|
"unplugin-vue-markdown": "^0.26.2",
|
||||||
|
"unplugin-vue-router": "^0.8.6",
|
||||||
|
"vite": "^5.2.10",
|
||||||
|
"vite-auto-import-resolvers": "^3.2.1",
|
||||||
|
"vite-layers": "^0.5.2",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
|
"vite-plugin-env-types": "^0.1.4",
|
||||||
|
"vite-plugin-legacy-swc": "^1.1.0",
|
||||||
|
"vite-plugin-use-modules": "^1.4.8",
|
||||||
|
"vite-plugin-vue-devtools": "^7.1.3",
|
||||||
|
"vite-plugin-vue-meta-layouts": "^0.4.3",
|
||||||
|
"vitest": "^1.5.3",
|
||||||
|
"vue": "^3.4.26",
|
||||||
|
"vue-echarts": "^6.7.1",
|
||||||
|
"vue-request": "2.0.4",
|
||||||
|
"vue-router": "^4.3.2",
|
||||||
|
"vue-toastification": "2.0.0-rc.5"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,jsx,ts,tsx,vue}": "eslint --cache --fix"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@latest",
|
||||||
|
"array-includes": "npm:@nolyfill/array-includes@latest",
|
||||||
|
"array.prototype.findlastindex": "npm:@nolyfill/array.prototype.findlastindex@latest",
|
||||||
|
"array.prototype.flat": "npm:@nolyfill/array.prototype.flat@latest",
|
||||||
|
"array.prototype.flatmap": "npm:@nolyfill/array.prototype.flatmap@latest",
|
||||||
|
"arraybuffer.prorotype.slice": "npm:@nolyfill/arraybuffer.prorotype.slice@latest",
|
||||||
|
"function.prototype.name": "npm:@nolyfill/function.prototype.name@latest",
|
||||||
|
"has": "npm:@nolyfill/has@latest",
|
||||||
|
"is-regex": "npm:@nolyfill/is-regex@latest",
|
||||||
|
"object-keys": "npm:@nolyfill/object-keys@latest",
|
||||||
|
"object.assign": "npm:@nolyfill/object.assign@latest",
|
||||||
|
"object.entries": "npm:@nolyfill/object.entries@latest",
|
||||||
|
"object.fromentries": "npm:@nolyfill/object.fromentries@latest",
|
||||||
|
"object.values": "npm:@nolyfill/object.values@latest",
|
||||||
|
"vue-demi": "npm:vue-demi@latest"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/dishait/tov-template"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
">= 0.25%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead",
|
||||||
|
"not ie <= 11",
|
||||||
|
"Android >= 4.0",
|
||||||
|
"iOS >= 8"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
|
"element-plus": "^2.9.2",
|
||||||
|
"save": "^2.9.0",
|
||||||
|
"scss": "^0.2.4",
|
||||||
|
"use-element-plus-theme": "^0.0.5",
|
||||||
|
"v-charts": "^1.19.0",
|
||||||
|
"vite-plugin-theme": "^0.8.6"
|
||||||
|
}
|
||||||
|
}
|
8598
ui/pnpm-lock.yaml
generated
Normal file
8598
ui/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
ui/presets/autoprefixer.ts
Normal file
33
ui/presets/autoprefixer.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import type { Preset } from 'unocss'
|
||||||
|
import browserslist from 'browserslist'
|
||||||
|
import { defaultBuildTargets } from './shared/detect'
|
||||||
|
import { browserslistToTargets, transformStyleAttribute } from 'lightningcss'
|
||||||
|
|
||||||
|
export default function autoprefixerPreset(
|
||||||
|
targets: string[] = defaultBuildTargets,
|
||||||
|
): Preset {
|
||||||
|
return {
|
||||||
|
name: 'unocss-preset-autoprefixer',
|
||||||
|
postprocess: (util) => {
|
||||||
|
const entries = util.entries
|
||||||
|
const { code } = transformStyleAttribute({
|
||||||
|
code: Buffer.from(
|
||||||
|
entries
|
||||||
|
.filter((item) => !item[0].startsWith('--un'))
|
||||||
|
.map((x) => x.join(':'))
|
||||||
|
.join(';'),
|
||||||
|
),
|
||||||
|
targets: browserslistToTargets(browserslist(targets)),
|
||||||
|
minify: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
util.entries = [
|
||||||
|
...entries.filter((item) => item[0].startsWith('--un')),
|
||||||
|
...(code
|
||||||
|
.toString()
|
||||||
|
.split(';')
|
||||||
|
.map((i) => i.split(':')) as [string, string | number][]),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
232
ui/presets/index.ts
Normal file
232
ui/presets/index.ts
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
import UnoCss from 'unocss/vite'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import {
|
||||||
|
AntDesignVueResolver,
|
||||||
|
ArcoResolver,
|
||||||
|
DevUiResolver,
|
||||||
|
ElementPlusResolver,
|
||||||
|
HeadlessUiResolver,
|
||||||
|
IduxResolver,
|
||||||
|
InklineResolver,
|
||||||
|
LayuiVueResolver,
|
||||||
|
NaiveUiResolver,
|
||||||
|
PrimeVueResolver,
|
||||||
|
QuasarResolver,
|
||||||
|
TDesignResolver,
|
||||||
|
VantResolver,
|
||||||
|
VarletUIResolver,
|
||||||
|
ViewUiResolver,
|
||||||
|
VueUseComponentsResolver,
|
||||||
|
Vuetify3Resolver,
|
||||||
|
} from 'unplugin-vue-components/resolvers'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import Markdown from 'unplugin-vue-markdown/vite'
|
||||||
|
import { VueRouterAutoImports } from 'unplugin-vue-router'
|
||||||
|
import Router from 'unplugin-vue-router/vite'
|
||||||
|
|
||||||
|
import { AutoGenerateImports, vue3Presets } from 'vite-auto-import-resolvers'
|
||||||
|
import Compression from 'vite-plugin-compression'
|
||||||
|
import EnvTypes from 'vite-plugin-env-types'
|
||||||
|
import Modules from 'vite-plugin-use-modules'
|
||||||
|
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
import Layouts from 'vite-plugin-vue-meta-layouts'
|
||||||
|
import Legacy from 'vite-plugin-legacy-swc'
|
||||||
|
import Vue from '@vitejs/plugin-vue'
|
||||||
|
import Jsx from '@vitejs/plugin-vue-jsx'
|
||||||
|
|
||||||
|
// 内置插件
|
||||||
|
import {
|
||||||
|
Alias,
|
||||||
|
Lightningcss,
|
||||||
|
Restart,
|
||||||
|
Warmup,
|
||||||
|
Layers,
|
||||||
|
Optimize,
|
||||||
|
} from './plugins'
|
||||||
|
import { defaultBuildTargets, detectResolvers, useEnv } from './shared/detect'
|
||||||
|
import { r } from './shared/path'
|
||||||
|
import type { PluginOption } from 'vite'
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const env = useEnv()
|
||||||
|
const safelist =
|
||||||
|
'prose px-2 sm:px-0 md:prose-lg lg:prose-lg dark:prose-invert text-left w-screen prose-slate prose-img:rounded-xl prose-headings:underline prose-a:text-blue-600'
|
||||||
|
|
||||||
|
const plugins: PluginOption[] = [
|
||||||
|
/**
|
||||||
|
* vite 配置层
|
||||||
|
* 通过 mode 区分 vite 配置文件 (experimental)
|
||||||
|
*/
|
||||||
|
Layers(),
|
||||||
|
/**
|
||||||
|
* 兼容不支持 esmModule 的浏览器
|
||||||
|
* https://www.npmjs.com/package/@vitejs/plugin-legacy
|
||||||
|
*/
|
||||||
|
Legacy({
|
||||||
|
targets: defaultBuildTargets,
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 智能启动 lightningcss
|
||||||
|
*/
|
||||||
|
Lightningcss(),
|
||||||
|
/**
|
||||||
|
* 启动优化
|
||||||
|
*/
|
||||||
|
Optimize(),
|
||||||
|
/**
|
||||||
|
* 环境变量类型提示
|
||||||
|
* https://github.com/dishait/vite-plugin-env-types
|
||||||
|
*/
|
||||||
|
EnvTypes({
|
||||||
|
dts: r('presets/types/env.d.ts'),
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 内置的预热,可以加快冷启动
|
||||||
|
*/
|
||||||
|
Warmup(),
|
||||||
|
/**
|
||||||
|
* 文件路由
|
||||||
|
* https://github.com/posva/unplugin-vue-router
|
||||||
|
*/
|
||||||
|
Router({
|
||||||
|
routesFolder: r('src/pages'),
|
||||||
|
dts: r('presets/types/type-router.d.ts'),
|
||||||
|
extensions: ['.md', '.vue', '.tsx', '.jsx'],
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 自动安装 vue 插件
|
||||||
|
* https://github.com/dishait/vite-plugin-use-modules
|
||||||
|
*/
|
||||||
|
Modules({
|
||||||
|
auto: true,
|
||||||
|
// 内部使用虚拟模块,运行在前端,所以不需要 r 重写路径
|
||||||
|
target: 'src/plugins',
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* vue 官方插件,用来解析 sfc 单文件组件
|
||||||
|
* https://www.npmjs.com/package/@vitejs/plugin-vue
|
||||||
|
*/
|
||||||
|
Vue({
|
||||||
|
include: [/\.vue$/, /\.md$/],
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 布局系统
|
||||||
|
* https://github.com/dishait/vite-plugin-vue-meta-layouts
|
||||||
|
*/
|
||||||
|
Layouts({
|
||||||
|
skipTopLevelRouteLayout: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件自动按需引入
|
||||||
|
* https://github.com/antfu/unplugin-vue-components
|
||||||
|
*/
|
||||||
|
Components({
|
||||||
|
directoryAsNamespace: true,
|
||||||
|
include: [/\.vue$/, /\.vue\?vue/, /\.[tj]sx$/, /\.md$/],
|
||||||
|
extensions: ['md', 'vue', 'tsx', 'jsx'],
|
||||||
|
dts: r('presets/types/components.d.ts'),
|
||||||
|
types: [
|
||||||
|
{
|
||||||
|
from: 'vue-router',
|
||||||
|
names: ['RouterLink', 'RouterView'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resolvers: detectResolvers({
|
||||||
|
onlyExist: [
|
||||||
|
[VantResolver(), 'vant'],
|
||||||
|
[QuasarResolver(), 'quasar'],
|
||||||
|
[DevUiResolver(), 'vue-devui'],
|
||||||
|
[NaiveUiResolver(), 'naive-ui'],
|
||||||
|
[Vuetify3Resolver(), 'vuetify'],
|
||||||
|
[PrimeVueResolver(), 'primevue'],
|
||||||
|
[ViewUiResolver(), 'view-design'],
|
||||||
|
[LayuiVueResolver(), 'layui-vue'],
|
||||||
|
[VarletUIResolver(), '@varlet/ui'],
|
||||||
|
[IduxResolver(), '@idux/components'],
|
||||||
|
[InklineResolver(), '@inkline/inkline'],
|
||||||
|
[ElementPlusResolver(), 'element-plus'],
|
||||||
|
[HeadlessUiResolver(), '@headlessui/vue'],
|
||||||
|
[ArcoResolver(), '@arco-design/web-vue'],
|
||||||
|
[AntDesignVueResolver({ importStyle: false }), 'ant-design-vue'],
|
||||||
|
[VueUseComponentsResolver(), '@vueuse/components'],
|
||||||
|
[TDesignResolver({ library: 'vue-next' }), 'tdesign-vue-next'],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* jsx 和 tsx 支持
|
||||||
|
* https://www.npmjs.com/package/@vitejs/plugin-vue-jsx
|
||||||
|
*/
|
||||||
|
Jsx(),
|
||||||
|
/**
|
||||||
|
* 生产环境资源压缩
|
||||||
|
* https://github.com/vbenjs/vite-plugin-compression
|
||||||
|
*/
|
||||||
|
Compression({
|
||||||
|
// @ts-ignore
|
||||||
|
algorithm: env.VITE_APP_COMPRESSINON_ALGORITHM,
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 别名插件 (内置)
|
||||||
|
* 支持 `~` 和 `@` 别名到 `src`
|
||||||
|
*/
|
||||||
|
Alias(),
|
||||||
|
/**
|
||||||
|
* 强制重启 (内置)
|
||||||
|
* 如果 package.json 或 pnpm-lock.yaml 更新的话,强制重启
|
||||||
|
*/
|
||||||
|
Restart(),
|
||||||
|
/**
|
||||||
|
* css 原子引擎
|
||||||
|
* https://github.com/unocss/unocss
|
||||||
|
*/
|
||||||
|
UnoCss({
|
||||||
|
safelist: env.VITE_APP_MARKDOWN ? safelist.split(' ') : undefined,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
/**
|
||||||
|
* 开发面板
|
||||||
|
* https://github.com/webfansplz/vite-plugin-vue-devtools
|
||||||
|
*/
|
||||||
|
if (env.VITE_APP_DEV_TOOLS) {
|
||||||
|
plugins.push(VueDevTools())
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* api 自动按需引入
|
||||||
|
* https://github.com/antfu/unplugin-auto-import
|
||||||
|
*/
|
||||||
|
if (env.VITE_APP_API_AUTO_IMPORT) {
|
||||||
|
const dirs = env.VITE_APP_DIR_API_AUTO_IMPORT
|
||||||
|
? ['src/stores/**', 'src/composables/**', 'src/api/**']
|
||||||
|
: []
|
||||||
|
plugins.push(
|
||||||
|
AutoImport({
|
||||||
|
dirs,
|
||||||
|
vueTemplate: true,
|
||||||
|
dts: r('presets/types/auto-imports.d.ts'),
|
||||||
|
imports: [
|
||||||
|
...AutoGenerateImports({
|
||||||
|
include: [...vue3Presets],
|
||||||
|
exclude: ['vue-router'],
|
||||||
|
}),
|
||||||
|
VueRouterAutoImports,
|
||||||
|
],
|
||||||
|
resolvers: detectResolvers({
|
||||||
|
onlyExist: [
|
||||||
|
[ElementPlusResolver(), 'element-plus'],
|
||||||
|
[TDesignResolver({ library: 'vue-next' }), 'tdesign-vue-next'],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
eslintrc: {
|
||||||
|
enabled: true,
|
||||||
|
globalsPropValue: true,
|
||||||
|
filepath: r('presets/eslint/.eslintrc-auto-import.json'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins
|
||||||
|
}
|
27
ui/presets/plugins/alias.ts
Normal file
27
ui/presets/plugins/alias.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import type { Plugin } from 'vite'
|
||||||
|
import { r } from '../shared/path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 别名插件
|
||||||
|
* @description 支持 `~` 和 `@` 别名到 `src`
|
||||||
|
*/
|
||||||
|
export function Alias(): Plugin {
|
||||||
|
const src = r('./src')
|
||||||
|
return {
|
||||||
|
name: 'vite-alias',
|
||||||
|
enforce: 'pre',
|
||||||
|
config(config) {
|
||||||
|
config.resolve ??= {}
|
||||||
|
config.resolve.alias = [
|
||||||
|
{
|
||||||
|
find: /^~/,
|
||||||
|
replacement: src,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@\//,
|
||||||
|
replacement: src + '/',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
6
ui/presets/plugins/index.ts
Normal file
6
ui/presets/plugins/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export { Alias } from './alias'
|
||||||
|
export { Layers } from './layers'
|
||||||
|
export { Warmup } from './warmup'
|
||||||
|
export { Restart } from './restart'
|
||||||
|
export { Optimize } from './optimize'
|
||||||
|
export { Lightningcss } from './lightningcss'
|
41
ui/presets/plugins/layers.ts
Normal file
41
ui/presets/plugins/layers.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { gray } from 'kolorist'
|
||||||
|
import { basename } from 'path'
|
||||||
|
import { r } from '../shared/path'
|
||||||
|
import { Restart } from './restart'
|
||||||
|
import { createConsola } from 'consola'
|
||||||
|
import type { Plugin, UserConfig } from 'vite'
|
||||||
|
import { Layers as loadLayer, detectMode } from 'vite-layers'
|
||||||
|
|
||||||
|
const logger = createConsola().withTag('layers')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* vite 配置层
|
||||||
|
* @description 通过 mode 区分 vite 配置文件 (experimental)
|
||||||
|
*/
|
||||||
|
export function Layers(): Plugin {
|
||||||
|
const mode = detectMode()
|
||||||
|
const modeFiles = [mode.slice(0, 3), mode].map((mode) =>
|
||||||
|
r(`vite.config.${mode}.ts`),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
...Restart(modeFiles.map((modeFile) => basename(modeFile))),
|
||||||
|
name: 'vite-plugin-layers',
|
||||||
|
enforce: 'post',
|
||||||
|
async config(config) {
|
||||||
|
const modeFile = modeFiles.find((modeFile) => existsSync(modeFile))
|
||||||
|
if (modeFile) {
|
||||||
|
logger
|
||||||
|
.withTag(mode)
|
||||||
|
.success(
|
||||||
|
`vite.config.ts → ${basename(modeFile)} ${gray(`(experimental)`)}`,
|
||||||
|
)
|
||||||
|
return loadLayer({
|
||||||
|
logger: false,
|
||||||
|
extends: [config, modeFile],
|
||||||
|
}) as UserConfig
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
62
ui/presets/plugins/lightningcss.ts
Normal file
62
ui/presets/plugins/lightningcss.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { gray } from 'kolorist'
|
||||||
|
import type { Plugin } from 'vite'
|
||||||
|
import { createConsola } from 'consola'
|
||||||
|
import { isPackageExists } from 'local-pkg'
|
||||||
|
import { browserslistToTargets } from 'lightningcss'
|
||||||
|
import { defaultBuildTargets } from '../shared/detect'
|
||||||
|
|
||||||
|
const name = 'vite-plugin-fire-lightningcss'
|
||||||
|
|
||||||
|
const logger = createConsola().withTag('css')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能开启 lightningcss (如果不使用预处理器,或者 postcss)
|
||||||
|
*/
|
||||||
|
export function Lightningcss(): Plugin {
|
||||||
|
const packages = ['less', 'sass', 'stylus']
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
config(config) {
|
||||||
|
config.css ??= {}
|
||||||
|
config.build ??= {}
|
||||||
|
const hasPreprocessor = packages.some((p) => isPackageExists(p))
|
||||||
|
|
||||||
|
const { postcss, modules, transformer } = config.css
|
||||||
|
const conflictConfiguration = [postcss, modules, transformer].some(
|
||||||
|
(c) => !isUndefined(c),
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasPostcssConfigFile = [
|
||||||
|
'postcss.config.js',
|
||||||
|
'postcss.config.cts',
|
||||||
|
'postcss.config.ts',
|
||||||
|
].some((c) => existsSync(c))
|
||||||
|
|
||||||
|
// 如果有预处理器,冲突配置或者 postcss 配置文件则禁用
|
||||||
|
const disabled =
|
||||||
|
hasPreprocessor || conflictConfiguration || hasPostcssConfigFile
|
||||||
|
if (!disabled) {
|
||||||
|
const transformer = 'lightningcss'
|
||||||
|
config.css.transformer = transformer
|
||||||
|
let tip = `${transformer} ${gray(transformer)}`
|
||||||
|
|
||||||
|
if (isUndefined(config.build.cssMinify)) {
|
||||||
|
config.build.cssMinify = 'lightningcss'
|
||||||
|
tip = `${transformer} ${gray('(transformer + cssMinify)')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUndefined(config.css.lightningcss?.targets)) {
|
||||||
|
config.css.lightningcss ??= {}
|
||||||
|
config.css.lightningcss.targets =
|
||||||
|
browserslistToTargets(defaultBuildTargets)
|
||||||
|
}
|
||||||
|
logger.success(tip)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUndefined(v: unknown): v is undefined {
|
||||||
|
return typeof v === 'undefined'
|
||||||
|
}
|
||||||
|
}
|
19
ui/presets/plugins/optimize.ts
Normal file
19
ui/presets/plugins/optimize.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { createConsola } from 'consola'
|
||||||
|
import { gray } from 'kolorist'
|
||||||
|
import type { Plugin } from 'vite'
|
||||||
|
|
||||||
|
const logger = createConsola().withTag('optimize')
|
||||||
|
export function Optimize(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'vite-optimize',
|
||||||
|
config(config) {
|
||||||
|
config.css ??= {}
|
||||||
|
config.optimizeDeps ??= {}
|
||||||
|
config.css.preprocessorMaxWorkers = true
|
||||||
|
config.optimizeDeps.holdUntilCrawlEnd = false
|
||||||
|
logger.success(
|
||||||
|
`optimize ${gray('(preprocessorMaxWorkers + closeHoldUntilCrawlEnd)')}`,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
30
ui/presets/plugins/restart.ts
Normal file
30
ui/presets/plugins/restart.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import type { Plugin } from 'vite'
|
||||||
|
import { utimes } from 'fs/promises'
|
||||||
|
import { r } from '../shared/path'
|
||||||
|
import { debounce } from 'perfect-debounce'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
import { slash } from 'vite-layers'
|
||||||
|
|
||||||
|
const defaultPaths = ['package.json', 'pnpm-lock.yaml']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制重启
|
||||||
|
* @description 如果监听更新的话,强制重启项目
|
||||||
|
* @param paths 监听重启路径,默认为 ['package.json', 'pnpm-lock.yaml']
|
||||||
|
*/
|
||||||
|
export function Restart(paths = defaultPaths): Plugin {
|
||||||
|
paths = paths.map((path) => slash(resolve(path)))
|
||||||
|
const restart = debounce(async function touch() {
|
||||||
|
const time = new Date()
|
||||||
|
await utimes(r('vite.config.ts'), time, time)
|
||||||
|
}, 1000)
|
||||||
|
return {
|
||||||
|
name: 'vite-plugin-force-restart',
|
||||||
|
apply: 'serve',
|
||||||
|
async watchChange(id) {
|
||||||
|
if (paths.includes(id)) {
|
||||||
|
await restart()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
21
ui/presets/plugins/warmup.ts
Normal file
21
ui/presets/plugins/warmup.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { Plugin } from 'vite'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预热
|
||||||
|
* @description 内置的预热,可以加快冷启动
|
||||||
|
*/
|
||||||
|
export function Warmup(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'vite-plugin-warmup',
|
||||||
|
apply: 'serve',
|
||||||
|
config(config) {
|
||||||
|
const src = './src/**/*'
|
||||||
|
config.server ??= {}
|
||||||
|
config.server.warmup ??= {}
|
||||||
|
config.server.warmup.clientFiles ??= []
|
||||||
|
if (!config.server.warmup.clientFiles.includes(src)) {
|
||||||
|
config.server.warmup.clientFiles.push(src)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
76
ui/presets/shared/detect.ts
Normal file
76
ui/presets/shared/detect.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* 侦察模块
|
||||||
|
* @description 自动检测环境并智能生成
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { r } from './path'
|
||||||
|
import { loadEnv } from 'vite'
|
||||||
|
import browserslist from 'browserslist'
|
||||||
|
import { detectMode } from 'vite-layers'
|
||||||
|
import { isPackageExists } from 'local-pkg'
|
||||||
|
import type { ComponentResolver } from 'unplugin-vue-components'
|
||||||
|
|
||||||
|
const { loadConfig: browserslistLoadConfig } = browserslist
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认打包目标 (浏览器兼容程度)
|
||||||
|
*/
|
||||||
|
export const defaultBuildTargets = browserslistLoadConfig({
|
||||||
|
path: r('./'),
|
||||||
|
}) || ['last 2 versions and not dead, > 0.3%, Firefox ESR']
|
||||||
|
|
||||||
|
type Arrayable<T> = T | Array<T>
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
onlyExist?: [Arrayable<ComponentResolver>, string][]
|
||||||
|
include?: ComponentResolver[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发现 resolvers
|
||||||
|
*/
|
||||||
|
export function detectResolvers(options: Options = {}) {
|
||||||
|
const { onlyExist = [], include = [] } = options
|
||||||
|
|
||||||
|
const existedResolvers = []
|
||||||
|
for (let i = 0; i < onlyExist.length; i++) {
|
||||||
|
const [resolver, packageName] = onlyExist[i]
|
||||||
|
if (
|
||||||
|
isPackageExists(packageName, {
|
||||||
|
paths: [r('./')],
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
existedResolvers.push(resolver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existedResolvers.push(...include)
|
||||||
|
|
||||||
|
return existedResolvers
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取环境变量
|
||||||
|
export function useEnv() {
|
||||||
|
function stringToBoolean(v: string) {
|
||||||
|
return Boolean(v === 'true' || false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
VITE_APP_TITLE,
|
||||||
|
VITE_APP_DEV_TOOLS,
|
||||||
|
VITE_APP_MARKDOWN,
|
||||||
|
VITE_APP_API_AUTO_IMPORT,
|
||||||
|
VITE_APP_MOCK_IN_PRODUCTION,
|
||||||
|
VITE_APP_DIR_API_AUTO_IMPORT,
|
||||||
|
VITE_APP_COMPRESSINON_ALGORITHM,
|
||||||
|
} = loadEnv(detectMode(), '.')
|
||||||
|
|
||||||
|
return {
|
||||||
|
VITE_APP_TITLE,
|
||||||
|
VITE_APP_COMPRESSINON_ALGORITHM,
|
||||||
|
VITE_APP_DEV_TOOLS: stringToBoolean(VITE_APP_DEV_TOOLS),
|
||||||
|
VITE_APP_MARKDOWN: stringToBoolean(VITE_APP_MARKDOWN),
|
||||||
|
VITE_APP_API_AUTO_IMPORT: stringToBoolean(VITE_APP_API_AUTO_IMPORT),
|
||||||
|
VITE_APP_MOCK_IN_PRODUCTION: stringToBoolean(VITE_APP_MOCK_IN_PRODUCTION),
|
||||||
|
VITE_APP_DIR_API_AUTO_IMPORT: stringToBoolean(VITE_APP_DIR_API_AUTO_IMPORT),
|
||||||
|
}
|
||||||
|
}
|
89
ui/presets/shared/mock.ts
Normal file
89
ui/presets/shared/mock.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
/**
|
||||||
|
* issue: https://github.com/vbenjs/vite-plugin-mock/issues/47
|
||||||
|
* fix: https://github.com/vbenjs/vite-plugin-mock/issues/47#issuecomment-982724613
|
||||||
|
*/
|
||||||
|
import Mock from 'mockjs'
|
||||||
|
|
||||||
|
export function createFetchSever(mockList: any[]) {
|
||||||
|
if (!window['originFetch']) {
|
||||||
|
window['originFetch'] = window.fetch
|
||||||
|
window.fetch = function (fetchUrl: string, init: any) {
|
||||||
|
const currentMock = mockList.find((mi) => fetchUrl.includes(mi.url))
|
||||||
|
if (currentMock) {
|
||||||
|
const result = createFetchReturn(currentMock, init)
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
return window['originFetch'](fetchUrl, init)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function __param2Obj__(url: string) {
|
||||||
|
const search = url.split('?')[1]
|
||||||
|
if (!search) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
return JSON.parse(
|
||||||
|
'{"' +
|
||||||
|
decodeURIComponent(search)
|
||||||
|
.replace(/"/g, '\\"')
|
||||||
|
.replace(/&/g, '","')
|
||||||
|
.replace(/=/g, '":"')
|
||||||
|
.replace(/\+/g, ' ') +
|
||||||
|
'"}',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function __Fetch2ExpressReqWrapper__(handle: () => any) {
|
||||||
|
return function (options: any) {
|
||||||
|
let result = null
|
||||||
|
if (typeof handle === 'function') {
|
||||||
|
const { body, method, url, headers } = options
|
||||||
|
|
||||||
|
let b = body
|
||||||
|
b = JSON.parse(body)
|
||||||
|
result = handle({
|
||||||
|
method,
|
||||||
|
body: b,
|
||||||
|
query: __param2Obj__(url),
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
result = handle
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mock.mock(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep = (delay = 0) => {
|
||||||
|
if (delay) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, delay)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createFetchReturn(mock: any, init) {
|
||||||
|
const { timeout, response } = mock
|
||||||
|
const mockFn = __Fetch2ExpressReqWrapper__(response)
|
||||||
|
const data = mockFn(init)
|
||||||
|
await sleep(timeout)
|
||||||
|
const result = {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
clone() {
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
text() {
|
||||||
|
return Promise.resolve(data)
|
||||||
|
},
|
||||||
|
json() {
|
||||||
|
return Promise.resolve(data)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
15
ui/presets/shared/path.ts
Normal file
15
ui/presets/shared/path.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { dirname, resolve } from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
const _dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
const root = resolve(_dirname, '../../')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径重写
|
||||||
|
* @param path 相对路径
|
||||||
|
* @returns 基于根目录的相对路径
|
||||||
|
*/
|
||||||
|
export function r(path: string) {
|
||||||
|
return resolve(root, path).replaceAll('\\', '/')
|
||||||
|
}
|
17
ui/presets/types/vite.d.ts
vendored
Normal file
17
ui/presets/types/vite.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-pages/client" />
|
||||||
|
/// <reference types="unplugin-vue-router/client" />
|
||||||
|
/// <reference types="vite-plugin-use-modules/client" />
|
||||||
|
/// <reference types="vite-plugin-vue-meta-layouts/client" />
|
||||||
|
|
||||||
|
declare module "*.vue" {
|
||||||
|
import type { DefineComponent } from "vue";
|
||||||
|
const component: DefineComponent<{}, {}, any>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.md" {
|
||||||
|
import { ComponentOptions } from "vue";
|
||||||
|
const Component: ComponentOptions;
|
||||||
|
export default Component;
|
||||||
|
}
|
BIN
ui/public/a1.png
Normal file
BIN
ui/public/a1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
ui/public/eeeab8ba-58af-4da2-a882-2112c680f0c2.jpg
Normal file
BIN
ui/public/eeeab8ba-58af-4da2-a882-2112c680f0c2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 488 KiB |
BIN
ui/public/icoimg.png
Normal file
BIN
ui/public/icoimg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
ui/public/login/e36341619bf8f04dcbdc6b01105a85a.png
Normal file
BIN
ui/public/login/e36341619bf8f04dcbdc6b01105a85a.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 342 KiB |
BIN
ui/public/loginimg.jpg
Normal file
BIN
ui/public/loginimg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 212 KiB |
1
ui/public/notFound/33.svg
Normal file
1
ui/public/notFound/33.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.4 KiB |
3
ui/renovate.json
Normal file
3
ui/renovate.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["github>unjs/renovate-config"]
|
||||||
|
}
|
76
ui/scripts/create.cjs
Normal file
76
ui/scripts/create.cjs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
const { existsSync } = require('fs')
|
||||||
|
|
||||||
|
const { showDir, showExt, moduleTypes } = require('./shared/base.cjs')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动创建
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
function create(plop) {
|
||||||
|
let exist = null
|
||||||
|
let modulePath = null
|
||||||
|
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '自动创建',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'list',
|
||||||
|
default: 'component',
|
||||||
|
message: '您希望生成哪种类型的模块?',
|
||||||
|
choices: moduleTypes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isMarkdown',
|
||||||
|
type: 'confirm',
|
||||||
|
message: '是否 markdown 类型?',
|
||||||
|
default: false,
|
||||||
|
// 如果是 page 类型需要询问是否为 markdown 类型
|
||||||
|
when({ type }) {
|
||||||
|
return type === 'page'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'input',
|
||||||
|
message({ type }) {
|
||||||
|
return `请输入 ${type} 的命名`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shouldReset',
|
||||||
|
type: 'confirm',
|
||||||
|
default: false,
|
||||||
|
message({ type }) {
|
||||||
|
return `目标 ${type} 已存在,是否重置?`
|
||||||
|
},
|
||||||
|
// 确认模块是否已存在,是则询问是否重置
|
||||||
|
when({ type, name, isMarkdown }) {
|
||||||
|
const dir = showDir(type)
|
||||||
|
const ext = showExt(type, isMarkdown)
|
||||||
|
modulePath = `src/${dir}/${name}.${ext}`
|
||||||
|
exist = existsSync(modulePath)
|
||||||
|
if (exist) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions(answer) {
|
||||||
|
const { type, shouldReset } = answer
|
||||||
|
if (exist && !shouldReset) {
|
||||||
|
throw new Error(`${type} 创建失败`)
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'add',
|
||||||
|
force: true,
|
||||||
|
path: `../${modulePath}`,
|
||||||
|
templateFile: `./template/${type}.hbs`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = create
|
36
ui/scripts/deps-fresh.cjs
Normal file
36
ui/scripts/deps-fresh.cjs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
const { execSync } = require('child_process')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动更新依赖
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
function depsFresh(plop) {
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '自动更新依赖',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'list',
|
||||||
|
default: 'patch',
|
||||||
|
message: '你希望发布一个什么版本?',
|
||||||
|
choices: ['patch', 'minor', 'major'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shouldWrite',
|
||||||
|
type: 'confirm',
|
||||||
|
default: false,
|
||||||
|
message: '是否直接更新?',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions(answer) {
|
||||||
|
const { type, shouldWrite } = answer
|
||||||
|
|
||||||
|
execSync(`npx taze ${type} ${shouldWrite ? '-w' : ''}`, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
})
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = depsFresh
|
65
ui/scripts/release.cjs
Normal file
65
ui/scripts/release.cjs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
const { createConsola } = require('consola')
|
||||||
|
const { execSync } = require('child_process')
|
||||||
|
const { repository } = require('../package.json')
|
||||||
|
const { gray } = require('kolorist')
|
||||||
|
const { simpleGit } = require('simple-git')
|
||||||
|
const logger = createConsola().withTag('release')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动发版
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
async function release(plop) {
|
||||||
|
const git = simpleGit()
|
||||||
|
|
||||||
|
const remotes = await git.getRemotes(true)
|
||||||
|
|
||||||
|
const urls = remotes.map((r) => {
|
||||||
|
return r.refs.push
|
||||||
|
.replace('git@github.com:', 'https://github.com/')
|
||||||
|
.replace('.git', '')
|
||||||
|
})
|
||||||
|
let allowRelease = false
|
||||||
|
if (!urls.includes(repository.url)) {
|
||||||
|
allowRelease = await logger.prompt(`是否发布到 ${gray(repository.url)}`, {
|
||||||
|
type: 'confirm',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
allowRelease = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowRelease) {
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '自动发版',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'list',
|
||||||
|
default: 'patch',
|
||||||
|
message: '你希望发布一个什么版本?',
|
||||||
|
choices: [
|
||||||
|
'patch',
|
||||||
|
'minor',
|
||||||
|
'major',
|
||||||
|
'prepatch',
|
||||||
|
'premajor',
|
||||||
|
'preminor',
|
||||||
|
'prerelease',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions(answer) {
|
||||||
|
const { type } = answer
|
||||||
|
execSync(
|
||||||
|
`npx changelogen --${type} --release && git push --follow-tags`,
|
||||||
|
{
|
||||||
|
stdio: 'inherit',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = release
|
75
ui/scripts/remove.cjs
Normal file
75
ui/scripts/remove.cjs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
const { unlinkSync } = require('fs')
|
||||||
|
const { readdir } = require('fs/promises')
|
||||||
|
const { basename } = require('path')
|
||||||
|
const { showDir, moduleTypes } = require('./shared/base.cjs')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动删除
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
function remove(plop) {
|
||||||
|
plop.setActionType('remove', (answers) => {
|
||||||
|
const { name, type, shouldRemove } = answers
|
||||||
|
const dir = showDir(type)
|
||||||
|
const target = `./src/${dir}/${name}`
|
||||||
|
if (shouldRemove) {
|
||||||
|
return unlinkSync(target)
|
||||||
|
}
|
||||||
|
throw new Error(`删除 ${target} 失败`)
|
||||||
|
})
|
||||||
|
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '自动删除',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'list',
|
||||||
|
message: '请选择您要删除的类型',
|
||||||
|
async choices() {
|
||||||
|
const entrys = await readdir('./src', {
|
||||||
|
recursive: false,
|
||||||
|
withFileTypes: true,
|
||||||
|
})
|
||||||
|
const dirs = entrys.filter((e) => e.isDirectory())
|
||||||
|
const types = moduleTypes.filter((type) => {
|
||||||
|
const dir = showDir(type)
|
||||||
|
return dirs.includes(`./src/${dir}`)
|
||||||
|
})
|
||||||
|
return types
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'list',
|
||||||
|
message({ type }) {
|
||||||
|
return `请选择您要删除的 ${type} 模块`
|
||||||
|
},
|
||||||
|
async choices({ type }) {
|
||||||
|
const dir = showDir(type)
|
||||||
|
const entrys = await readdir(`src/${dir}`, {
|
||||||
|
recursive: false,
|
||||||
|
withFileTypes: true,
|
||||||
|
})
|
||||||
|
let modules = entrys.filter((e) => e.isFile())
|
||||||
|
modules = modules.map((module) => {
|
||||||
|
return basename(module)
|
||||||
|
})
|
||||||
|
return modules
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shouldRemove',
|
||||||
|
type: 'confirm',
|
||||||
|
default: false,
|
||||||
|
message: '再次确认是否删除',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'remove',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = remove
|
113
ui/scripts/safe-init.cjs
Normal file
113
ui/scripts/safe-init.cjs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
const { resolve } = require('path')
|
||||||
|
const { gray, green } = require('kolorist')
|
||||||
|
const { createConsola } = require('consola')
|
||||||
|
const { existsSync, lstatSync } = require('fs')
|
||||||
|
const { removeSync, emptyDirSync } = require('fs-extra')
|
||||||
|
|
||||||
|
function slash(path) {
|
||||||
|
return path.replace(/\\/g, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function r(dir) {
|
||||||
|
return slash(resolve(__dirname, '../', dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
const entrys = [
|
||||||
|
'src/components',
|
||||||
|
'src/api',
|
||||||
|
'mock',
|
||||||
|
'layouts/default.vue',
|
||||||
|
'src/pages/index.vue',
|
||||||
|
'src/pages/about.md',
|
||||||
|
'src/pages/echarts.vue',
|
||||||
|
'src/stores',
|
||||||
|
'locales/简体中文',
|
||||||
|
'locales/English',
|
||||||
|
]
|
||||||
|
|
||||||
|
const resolvedEntrys = entrys.map((entry) => r(entry))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全初始化
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
function safeInit(plop) {
|
||||||
|
const logger = createConsola().withTag('safe:init')
|
||||||
|
|
||||||
|
logger.warn('实验性功能')
|
||||||
|
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '安全初始化',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'yes',
|
||||||
|
type: 'confirm',
|
||||||
|
message: '是否安全的初始化?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cleanStyles',
|
||||||
|
type: 'confirm',
|
||||||
|
message: '是否清理 styles?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions(answer) {
|
||||||
|
if (!answer.yes) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (answer.cleanStyles) {
|
||||||
|
resolvedEntrys.push(r('src/styles'))
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log()
|
||||||
|
|
||||||
|
// 这里不用异步是因为 plop action 只支持同步
|
||||||
|
resolvedEntrys.forEach((e) => {
|
||||||
|
if (!existsSync(e)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const entry = lstatSync(e)
|
||||||
|
if (entry.isFile()) {
|
||||||
|
removeSync(e)
|
||||||
|
logClean(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
emptyDirSync(e)
|
||||||
|
logClean(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'add',
|
||||||
|
force: true,
|
||||||
|
path: '../src/pages/index.vue',
|
||||||
|
templateFile: './template/page.hbs',
|
||||||
|
data: {
|
||||||
|
name: 'index',
|
||||||
|
isMarkdown: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'add',
|
||||||
|
force: true,
|
||||||
|
path: '../src/layouts/default.vue',
|
||||||
|
templateFile: './template/layout.hbs',
|
||||||
|
data: {
|
||||||
|
name: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function logClean(path) {
|
||||||
|
console.log(`${green('√ clean')} ${gray(path)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = safeInit
|
41
ui/scripts/shared/base.cjs
Normal file
41
ui/scripts/shared/base.cjs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* 获取扩展名
|
||||||
|
* @param {string} type 模块类型
|
||||||
|
* @param {boolean} isMarkdown 是否是 markdown,默认为 false
|
||||||
|
* @returns {string} 扩展名
|
||||||
|
*/
|
||||||
|
const showExt = (type, isMarkdown = false) => {
|
||||||
|
const isTs = type === 'api' || type === 'store' || type === 'module'
|
||||||
|
const ext = isMarkdown ? 'md' : isTs ? 'ts' : 'vue'
|
||||||
|
return ext
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块类型
|
||||||
|
*/
|
||||||
|
const moduleTypes = [
|
||||||
|
'api',
|
||||||
|
'page',
|
||||||
|
'store',
|
||||||
|
'layout',
|
||||||
|
'module',
|
||||||
|
'component',
|
||||||
|
'composable',
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取目录
|
||||||
|
* @param {string} type 类型
|
||||||
|
*/
|
||||||
|
const showDir = (type) => {
|
||||||
|
if (type === 'api') {
|
||||||
|
return 'api'
|
||||||
|
}
|
||||||
|
return `${type}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
showExt,
|
||||||
|
showDir,
|
||||||
|
moduleTypes,
|
||||||
|
}
|
2
ui/scripts/template/api.hbs
Normal file
2
ui/scripts/template/api.hbs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
|
11
ui/scripts/template/component.hbs
Normal file
11
ui/scripts/template/component.hbs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{name}}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
5
ui/scripts/template/composable.hbs
Normal file
5
ui/scripts/template/composable.hbs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
|
export default {{name}} = () => {
|
||||||
|
|
||||||
|
}
|
4
ui/scripts/template/layout.hbs
Normal file
4
ui/scripts/template/layout.hbs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<template>
|
||||||
|
{{pascalCase name}} layout
|
||||||
|
<router-view />
|
||||||
|
</template>
|
6
ui/scripts/template/module.hbs
Normal file
6
ui/scripts/template/module.hbs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { App } from "vue"
|
||||||
|
|
||||||
|
|
||||||
|
export default (app: App) => {
|
||||||
|
|
||||||
|
}
|
17
ui/scripts/template/page.hbs
Normal file
17
ui/scripts/template/page.hbs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{{#if isMarkdown}}
|
||||||
|
## {{pascalCase name}} Page
|
||||||
|
|
||||||
|
> The page is markdown file
|
||||||
|
{{else}}
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{pascalCase name}} page
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
{{/if}}
|
9
ui/scripts/template/store.hbs
Normal file
9
ui/scripts/template/store.hbs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export default defineStore('{{name}}', {
|
||||||
|
state() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
getters: {},
|
||||||
|
actions: {}
|
||||||
|
})
|
13
ui/src/App.vue
Normal file
13
ui/src/App.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
p {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
</style>
|
12
ui/src/api/itemApi.ts
Normal file
12
ui/src/api/itemApi.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 分页展示
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function itemPage(data: any) {
|
||||||
|
|
||||||
|
return frontRequest.get("/api/item/page",{
|
||||||
|
params: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
50
ui/src/api/user/adminUserApi.ts
Normal file
50
ui/src/api/user/adminUserApi.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { adminRequest } from '~/composables/adminRequest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function captchaAdmin(uid: Number) {
|
||||||
|
return adminRequest.get("/captcha",{
|
||||||
|
params:{uuid:uid}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function loginAdmin(data: any) {
|
||||||
|
return adminRequest.post("/login", data)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 注册
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function registerAdmin(data: any) {
|
||||||
|
return adminRequest.post("/register", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出
|
||||||
|
*/
|
||||||
|
export function logoutAdmin() {
|
||||||
|
return adminRequest.post("/logout")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @param userId
|
||||||
|
*/
|
||||||
|
export function userInfoAdmin() {
|
||||||
|
return adminRequest.get("/sys/user/info")
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 修改密码
|
||||||
|
* @param userId
|
||||||
|
*/
|
||||||
|
export function updatePasswordAdmin(data:any) {
|
||||||
|
return adminRequest.put("/sys/user/password",data)
|
||||||
|
}
|
14
ui/src/api/user/adminUserUtils.ts
Normal file
14
ui/src/api/user/adminUserUtils.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { getUuid } from '~/utils/utils'
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取验证码图
|
||||||
|
*/
|
||||||
|
export function getCaptchaUrl(){
|
||||||
|
const uuid = getUuid()
|
||||||
|
captchaAdmin(uuid)
|
||||||
|
return {
|
||||||
|
uid: uuid,
|
||||||
|
captchaUrl:import.meta.env.VITE_ADMIN_API_BASE_URL + `/captcha?uuid=${uuid}`
|
||||||
|
}
|
||||||
|
}
|
34
ui/src/api/user/frontUserApi.ts
Normal file
34
ui/src/api/user/frontUserApi.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* 注册
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function registerFront(data: any) {
|
||||||
|
return frontRequest.post("/api/user/register", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @param userId
|
||||||
|
*/
|
||||||
|
export function userInfoFront(userId: any) {
|
||||||
|
return frontRequest.get("/api/user/userInfo", {
|
||||||
|
params: { userId: userId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改用户信息
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function userUpdateFront(data:any) {
|
||||||
|
return frontRequest.put("/api/user/update", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出
|
||||||
|
*/
|
||||||
|
export function logoutFront() {
|
||||||
|
return frontRequest.post("/api/user/logout")
|
||||||
|
}
|
60
ui/src/components/BarChart.vue
Normal file
60
ui/src/components/BarChart.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chart-container" ref="chartRef"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
const chartRef = ref(null)
|
||||||
|
let chartInstance = null
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
// 使用 axios 请求数据
|
||||||
|
const response = await adminRequest.get('/sys/item/view') // 修改为你自己的 API URL
|
||||||
|
const data = response.data // 假设返回的数据是一个数组,格式类似于:[{ name: 'A', value: 100 }, ...]
|
||||||
|
|
||||||
|
// 更新图表配置
|
||||||
|
const options = {
|
||||||
|
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.map((item) => item.year),
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: data.map((item) => item.count),
|
||||||
|
type: 'line',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置图表配置项
|
||||||
|
chartInstance.setOption(options)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据请求失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
chartInstance = echarts.init(chartRef.value)
|
||||||
|
// 调用 fetchData 获取数据并更新图表
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.dispose()
|
||||||
|
chartInstance = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
147
ui/src/components/Heads.vue
Normal file
147
ui/src/components/Heads.vue
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<div class="head">
|
||||||
|
<div class="head_l">
|
||||||
|
</div>
|
||||||
|
<el-dropdown>
|
||||||
|
<div class="head_r">
|
||||||
|
<!-- <img :src="userStore().adminUserInfo.avatar" alt="头像" class="profile" />-->
|
||||||
|
<div class="head_user">
|
||||||
|
<div class="head_user_name">{{ userStore().adminUserInfo.username }}</div>
|
||||||
|
<div class="head_user_desc">管理员</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item @click="drawer = true" >个人中心</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
|
||||||
|
<el-drawer
|
||||||
|
v-model="drawer"
|
||||||
|
title="个人中心"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="state.dynamicValidateForm"
|
||||||
|
label-position="top"
|
||||||
|
>
|
||||||
|
<el-form-item
|
||||||
|
prop="password"
|
||||||
|
label="原始密码"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '原始密码不能为空',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<el-input v-model="state.dynamicValidateForm.password" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item
|
||||||
|
prop="newPassword"
|
||||||
|
label="新密码"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '新密码不能为空',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<el-input v-model="state.dynamicValidateForm.newPassword" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submitForm(formRef)">确定修改</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import type { FormInstance } from 'element-plus'
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
import { updatePasswordAdmin } from '~/api/user/adminUserApi'
|
||||||
|
const router = useRouter();
|
||||||
|
const drawer = ref(false)
|
||||||
|
const state = reactive({
|
||||||
|
dynamicValidateForm:{}
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
const logout = () => {
|
||||||
|
logoutAdmin().then(()=>{
|
||||||
|
toast.success("退出成功~")
|
||||||
|
router.push('/login');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 修改密码
|
||||||
|
* @param formEl
|
||||||
|
*/
|
||||||
|
const submitForm = (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
formEl.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
updatePasswordAdmin(state.dynamicValidateForm).then(result => {
|
||||||
|
ElMessage.success("修改成功")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.head{
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
background: #fdfdfe;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
.head_l{
|
||||||
|
width:40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.head_l img{
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.head_r{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 30px;
|
||||||
|
.profile{
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f6f6f6;
|
||||||
|
color: #333333;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.head_user{
|
||||||
|
.head_user_name{
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #333333;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.head_user_desc{
|
||||||
|
color:#a7a7a7;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
92
ui/src/components/ImageUpload.vue
Normal file
92
ui/src/components/ImageUpload.vue
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-upload
|
||||||
|
class="avatar-uploader"
|
||||||
|
:action="state.path"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-success="handleSuccess"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
>
|
||||||
|
<img v-if="state.imageUrl" :src="state.imageUrl" class="avatar" />
|
||||||
|
|
||||||
|
<el-icon v-else class="avatar-uploader-icon">
|
||||||
|
<Plus />
|
||||||
|
</el-icon>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
imageUrl: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建 emit 事件
|
||||||
|
import { UploadProps } from 'element-plus'
|
||||||
|
const emit = defineEmits();
|
||||||
|
const state = reactive({
|
||||||
|
imageUrl: '',
|
||||||
|
path:import.meta.env.VITE_API_FRONT_BASE_URL+"/api/upload"
|
||||||
|
})
|
||||||
|
// 监控 props.imageUrl 的变化
|
||||||
|
watch(
|
||||||
|
() => props.imageUrl, // 监听 imageUrl 的变化
|
||||||
|
(newVal, oldVal) => {
|
||||||
|
// console.log('imageUrl changed from', oldVal, 'to', newVal)
|
||||||
|
// 这里可以执行相应的操作,比如清空本地 state.imageUrl 或更新界面
|
||||||
|
state.imageUrl = newVal // 更新本地的 imageUrl
|
||||||
|
}
|
||||||
|
)
|
||||||
|
onMounted(()=>{
|
||||||
|
if (props.imageUrl){
|
||||||
|
state.imageUrl = props.imageUrl
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 上传前的钩子,返回 false 可以阻止上传
|
||||||
|
function beforeUpload(file) {
|
||||||
|
const isImage = file.type.startsWith('image/')
|
||||||
|
if (!isImage) {
|
||||||
|
ElMessage.error('只能上传图片文件!')
|
||||||
|
}
|
||||||
|
return isImage
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传成功后的回调
|
||||||
|
const handleSuccess: UploadProps['onSuccess'] = (
|
||||||
|
response,
|
||||||
|
uploadFile
|
||||||
|
) => {
|
||||||
|
state.imageUrl = response.data.path
|
||||||
|
ElMessage.success('上传成功')
|
||||||
|
console.log("fanhui1",response)
|
||||||
|
console.log("fanhui2",response.data.path)
|
||||||
|
// 通过 emit 将路径传递给父组件
|
||||||
|
emit('update:imageUrl', response.data.path);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.el-upload ){
|
||||||
|
border: 1px dashed var(--el-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: var(--el-transition-duration-fast);
|
||||||
|
}
|
||||||
|
.el-icon.avatar-uploader-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.avatar{
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
}
|
||||||
|
</style>
|
110
ui/src/components/e-editor.vue
Normal file
110
ui/src/components/e-editor.vue
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div >
|
||||||
|
<Toolbar
|
||||||
|
style="border-bottom: 1px solid #ccc"
|
||||||
|
:editor="editorRef"
|
||||||
|
:defaultConfig="toolbarConfig"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
<!-- 内容-->
|
||||||
|
<Editor
|
||||||
|
style="height: 500px; overflow-y: hidden"
|
||||||
|
v-model="valueHtml"
|
||||||
|
:defaultConfig="editorConfig"
|
||||||
|
:mode="mode"
|
||||||
|
@onCreated="handleCreated"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import '@wangeditor/editor/dist/css/style.css'
|
||||||
|
import { onBeforeUnmount, ref, shallowRef, reactive } from 'vue'
|
||||||
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
// 编辑器实例,必须用 shallowRef
|
||||||
|
const editorRef = shallowRef<InstanceType<typeof Editor> | null>(null)
|
||||||
|
const emit = defineEmits();
|
||||||
|
const state = reactive({
|
||||||
|
path: import.meta.env.VITE_API_FRONT_BASE_URL + '/api/upload',
|
||||||
|
})
|
||||||
|
const props = defineProps({
|
||||||
|
content: { type: String, default: '' },
|
||||||
|
});
|
||||||
|
// 内容 HTML
|
||||||
|
const valueHtml = ref<string>('')
|
||||||
|
// watch监听 更新编辑器内容
|
||||||
|
watch(
|
||||||
|
() => props.content,
|
||||||
|
(newContent) => {
|
||||||
|
valueHtml.value = newContent;
|
||||||
|
emit( "update:content",valueHtml.value)
|
||||||
|
},
|
||||||
|
{ immediate: true } // immediate 确保第一次渲染时就执行一次回调
|
||||||
|
);
|
||||||
|
watch(() => valueHtml.value,
|
||||||
|
(newContent) => {
|
||||||
|
emit( "update:content",valueHtml.value)
|
||||||
|
},
|
||||||
|
{ immediate: true } // immediate 确保第一次渲染时就执行一次回调
|
||||||
|
);
|
||||||
|
|
||||||
|
// 工具栏配置
|
||||||
|
const toolbarConfig = {
|
||||||
|
toolbarKeys: [
|
||||||
|
// 一些常用的菜单 key
|
||||||
|
"bold", // 加粗
|
||||||
|
"italic", // 斜体
|
||||||
|
"through", // 删除线
|
||||||
|
"underline", // 下划线
|
||||||
|
"bulletedList", // 无序列表
|
||||||
|
"numberedList", // 有序列表
|
||||||
|
"color", // 文字颜色
|
||||||
|
"fontSize", // 字体大小
|
||||||
|
"lineHeight", // 行高
|
||||||
|
"uploadImage", // 上传图片
|
||||||
|
"delIndent", // 缩进
|
||||||
|
"indent", // 增进
|
||||||
|
"deleteImage", //删除图片
|
||||||
|
"divider", // 分割线
|
||||||
|
"justifyCenter", // 居中对齐
|
||||||
|
"justifyJustify", // 两端对齐
|
||||||
|
"justifyLeft", // 左对齐
|
||||||
|
"justifyRight", // 右对齐
|
||||||
|
"undo", // 撤销
|
||||||
|
"redo", // 重做
|
||||||
|
"clearStyle", // 清除格式
|
||||||
|
],
|
||||||
|
};
|
||||||
|
// 编辑器配置
|
||||||
|
const editorConfig = {
|
||||||
|
placeholder: '请输入内容...',
|
||||||
|
MENU_CONF: {
|
||||||
|
uploadImage: {
|
||||||
|
server: state.path,
|
||||||
|
fieldName: 'file',
|
||||||
|
maxFileSize: 20 * 1024 * 1024, // 20MB
|
||||||
|
allowedFileTypes: ['image/jpeg', 'image/png', 'image/gif'], // 允许的图片类型
|
||||||
|
customInsert(res, insertFn) {
|
||||||
|
if (res.code == 0) {
|
||||||
|
insertFn(res.data.path, null, res.data.path);
|
||||||
|
ElMessage.success("上传成功")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// 组件销毁时,及时销毁编辑器
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
const editor = editorRef.value
|
||||||
|
if (editor == null) return
|
||||||
|
editor.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 编辑器创建时的回调
|
||||||
|
const handleCreated = (editor: InstanceType<typeof Editor>) => {
|
||||||
|
editorRef.value = editor
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模式:default 或 simple
|
||||||
|
const mode = 'default'
|
||||||
|
</script>
|
39
ui/src/components/front/carousel.vue
Normal file
39
ui/src/components/front/carousel.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-carousel :height="height" motion-blur>
|
||||||
|
<el-carousel-item v-for="item in state.item" :key="item">
|
||||||
|
<el-image :src="item.path" fit="fill" />
|
||||||
|
</el-carousel-item>
|
||||||
|
</el-carousel>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '300px',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const state = reactive({
|
||||||
|
item:[]
|
||||||
|
})
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
onMounted(()=>{
|
||||||
|
frontRequest.get("/api/slides/page").then(res =>{
|
||||||
|
state.item = res.data.list
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
:deep(.el-image){
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
97
ui/src/components/front/info-bottom.vue
Normal file
97
ui/src/components/front/info-bottom.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<!--详情底部-->
|
||||||
|
<template>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
|
||||||
|
<div class="comment" v-for="item in state.commentList">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="3" ><span style="color: #598bd3;">{{ item.nickName }}</span></el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<span class="time">
|
||||||
|
{{item.createTime}}
|
||||||
|
</span>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<div class="content">
|
||||||
|
{{ item.content }}
|
||||||
|
</div>
|
||||||
|
<el-divider />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</el-col>
|
||||||
|
<!--热门-->
|
||||||
|
<el-col :span="12">
|
||||||
|
<div v-html="state.info.description"></div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
const route = useRoute();
|
||||||
|
const state =reactive(<any>{
|
||||||
|
commentList:[],
|
||||||
|
content:"",
|
||||||
|
info:""
|
||||||
|
})
|
||||||
|
|
||||||
|
const itemId = route.params.id;
|
||||||
|
function init() {
|
||||||
|
frontRequest.get(`/api/item/${itemId}`).then(response =>{
|
||||||
|
state.info = response.data
|
||||||
|
})
|
||||||
|
frontRequest.get("/api/comment/list",{
|
||||||
|
params:{itemId:itemId}
|
||||||
|
}).then(response =>{
|
||||||
|
state.commentList = response.data
|
||||||
|
console.log( response.data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function comment() {
|
||||||
|
if (state.content ==""){
|
||||||
|
ElMessage.error("内容不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
frontRequest.post("/api/comment",{itemId:itemId,content:state.content}).then(response =>{
|
||||||
|
ElMessage.success("发表成功")
|
||||||
|
state.comment=""
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(()=>{
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.comment{
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
.content{
|
||||||
|
padding-top: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #111;
|
||||||
|
text-indent: 2em; /* 中文缩进 */
|
||||||
|
}
|
||||||
|
.time{
|
||||||
|
color: #598bd3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.el-image){
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 420px;
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit{
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
:deep(.el-input__wrapper){
|
||||||
|
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
265
ui/src/components/front/item.vue
Normal file
265
ui/src/components/front/item.vue
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<el-row v-if="isPage" justify="center">
|
||||||
|
<el-col :span="24" class="search-container">
|
||||||
|
<el-input
|
||||||
|
v-model="state.page.title"
|
||||||
|
placeholder="搜索厨房用具"
|
||||||
|
class="search-input"
|
||||||
|
clearable
|
||||||
|
@input="init"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button :icon="Search" @click="init" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 品牌标签云 -->
|
||||||
|
<el-row v-if="isPage" class="brand-container">
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="brand-list">
|
||||||
|
<el-tag
|
||||||
|
v-for="brand in state.brands"
|
||||||
|
:key="brand"
|
||||||
|
class="brand-item"
|
||||||
|
effect="plain"
|
||||||
|
round
|
||||||
|
>
|
||||||
|
{{ brand.name }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 房源卡片 -->
|
||||||
|
<el-row class="product-container">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-space wrap :size="20">
|
||||||
|
<el-card
|
||||||
|
v-for="item in getList"
|
||||||
|
:key="item.id"
|
||||||
|
class="product-card"
|
||||||
|
@click="to(item.id)"
|
||||||
|
shadow="hover"
|
||||||
|
>
|
||||||
|
<div class="image-container">
|
||||||
|
<el-image :src="item.image" class="product-image" />
|
||||||
|
<el-tag v-if="item.tag" class="product-tag">{{ item.tag }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="product-info">
|
||||||
|
<div class="product-title">{{ item.title }}</div>
|
||||||
|
<div class="price-container">
|
||||||
|
<span class="current-price">¥{{ item.price }}</span>
|
||||||
|
<el-tag type="success" size="small">区块溯源</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-space>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<el-row v-if="isPage" class="pagination-container">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:total="state.total"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
class="custom-pagination"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { itemPage } from '~/api/itemApi'
|
||||||
|
import { Search } from '@element-plus/icons-vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { computed, reactive, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const props = defineProps({
|
||||||
|
isPage: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
getList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [], // 默认值为空数组
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算属性来判断 list 是否为空,如果为空则使用 props.getList
|
||||||
|
const getList = computed(() => {
|
||||||
|
return state.getList.length > 0 ? state.getList : props.getList
|
||||||
|
})
|
||||||
|
|
||||||
|
const state = reactive(<any>{
|
||||||
|
total: 0,
|
||||||
|
page: {
|
||||||
|
page: 1,
|
||||||
|
title: '',
|
||||||
|
limit:12
|
||||||
|
},
|
||||||
|
getList: [],
|
||||||
|
brands:[]
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化
|
||||||
|
*/
|
||||||
|
function init() {
|
||||||
|
if (props.isPage) {
|
||||||
|
itemPage(state.page).then((res) => {
|
||||||
|
state.getList = res.data.list
|
||||||
|
state.total = res.data.total
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
frontRequest.get("/api/categories/page").then((res) => {
|
||||||
|
state.brands = res.data.list
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页
|
||||||
|
*/
|
||||||
|
const handleCurrentChange = (val: number) => {
|
||||||
|
state.page.page = val
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转
|
||||||
|
*/
|
||||||
|
const to = (id: number) => {
|
||||||
|
router.push(`/info/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
/* 搜索框样式 */
|
||||||
|
.search-container {
|
||||||
|
padding: 20px 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.search-input {
|
||||||
|
width: 652px;
|
||||||
|
height: 48px;
|
||||||
|
border: 2px solid #ef1f1f;
|
||||||
|
border-radius: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 品牌标签样式 */
|
||||||
|
.brand-container {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
}
|
||||||
|
.brand-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.brand-item {
|
||||||
|
padding: 8px 20px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.brand-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
background-color: #fef0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 房源卡片样式 */
|
||||||
|
.product-container {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
.product-card {
|
||||||
|
width: 240px;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.product-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
position: relative;
|
||||||
|
height: 260px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.product-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
.product-image:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
.product-tag {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
|
||||||
|
background-color: #b9ccf6;
|
||||||
|
color: #010110;
|
||||||
|
}
|
||||||
|
:deep(.el-card__body){
|
||||||
|
padding-top: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 房源信息 */
|
||||||
|
.product-info {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.product-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
height: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
.product-title:hover {
|
||||||
|
color: #c27006;
|
||||||
|
}
|
||||||
|
.price-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.current-price {
|
||||||
|
color: #ff5722;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页样式 */
|
||||||
|
.pagination-container {
|
||||||
|
padding: 30px 0;
|
||||||
|
}
|
||||||
|
.custom-pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
90
ui/src/components/front/item2.vue
Normal file
90
ui/src/components/front/item2.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<!--房源展示风格2-->
|
||||||
|
<template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-space wrap>
|
||||||
|
<el-card
|
||||||
|
v-for="item in state.commit" :key="state.id"
|
||||||
|
class="box-card"
|
||||||
|
@click="to(item.id)"
|
||||||
|
shadow="hover" >
|
||||||
|
<!--图片-->
|
||||||
|
<img class="img" :src="item.largePic" fit="fill" />
|
||||||
|
<!-- 标题-->
|
||||||
|
<div class="item-title ">{{item.title}}</div>
|
||||||
|
<!-- 价格&& 评分-->
|
||||||
|
<div class="price-box">
|
||||||
|
<span class="price-now ft16 " >{{item.movieType}}</span>
|
||||||
|
<span class="ft14 c999 price-old" >{{item.ratingValue}} 分</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-space>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const router = useRouter()
|
||||||
|
const state = reactive({
|
||||||
|
commit:[]
|
||||||
|
})
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
frontRequest.get("/api/item/commit").then(res =>{
|
||||||
|
state.commit = res.data
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
onMounted(() =>{
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
|
||||||
|
const to = (id:number) => {
|
||||||
|
router.push(`/info/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.box-card{
|
||||||
|
cursor: pointer;
|
||||||
|
width: 232px;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
.img{
|
||||||
|
display: block;
|
||||||
|
width: 170px;
|
||||||
|
height: 180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.item-title{
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
width: 164px;
|
||||||
|
margin: 0 auto 8px auto;
|
||||||
|
line-height: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.price-box{
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.price-now{
|
||||||
|
color: #FF0000;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
.ft16 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.ft14 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.price-old{
|
||||||
|
color: #fce04b;
|
||||||
|
}
|
||||||
|
</style>
|
114
ui/src/components/hot/Hot1.vue
Normal file
114
ui/src/components/hot/Hot1.vue
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<!--房源-->
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div style="height: 20px"></div>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<span style="font-weight: 700;font-size: 26px">狗狗主粮</span>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="containa">
|
||||||
|
<el-space wrap>
|
||||||
|
<div v-for="(i, index) in 10" :key="i">
|
||||||
|
<div
|
||||||
|
@click="to()"
|
||||||
|
class="box-card"
|
||||||
|
v-if="index === 0 || index === 5">
|
||||||
|
<el-image
|
||||||
|
style="width: 230px;height: 300px;"
|
||||||
|
src="https://img2.epetbar.com/common/upload/commonfile/2020/03/20/0104650_205628.jpg"
|
||||||
|
fit="fill" />
|
||||||
|
</div>
|
||||||
|
<el-card
|
||||||
|
v-else
|
||||||
|
class="box-card"
|
||||||
|
@click="to()"
|
||||||
|
shadow="hover" >
|
||||||
|
<!-- 图片-->
|
||||||
|
<el-image src="https://img2.epetbar.com/common/upload/commonfile/2020/03/20/0104650_205628.jpg" fit="fill" />
|
||||||
|
<!-- 标题-->
|
||||||
|
<div class="item-title ">澳大利亚原装进口自然馈赠Natures Gift 牛肉配方成犬粮 18kg</div>
|
||||||
|
<!-- 价格&& 评分-->
|
||||||
|
<div class="price-box">
|
||||||
|
<span class="price-now ft16 " style="line-height: 1">¥998.00</span>
|
||||||
|
<span class="ft14 c999 price-old" style="line-height: 1">¥1098.00</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-space>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const router = useRouter()
|
||||||
|
// const props = defineProps({
|
||||||
|
// isPage: {
|
||||||
|
// type: Boolean,
|
||||||
|
// default: false,
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
|
||||||
|
const to = () => {
|
||||||
|
router.push("/front/info")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.containa{
|
||||||
|
padding-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.box-card{
|
||||||
|
cursor: pointer;
|
||||||
|
width: 232px;
|
||||||
|
height: 300px
|
||||||
|
}
|
||||||
|
.box-card1{
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
height: 300px
|
||||||
|
}
|
||||||
|
:deep(.el-image){
|
||||||
|
display: block;
|
||||||
|
width: 170px;
|
||||||
|
height: 180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
}
|
||||||
|
.item-title{
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
width: 164px;
|
||||||
|
margin: 0 auto 8px auto;
|
||||||
|
line-height: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.price-box{
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.price-now{
|
||||||
|
color: #FF0000;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
.ft16 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.ft14 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container{
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
97
ui/src/components/nav/Navigation.vue
Normal file
97
ui/src/components/nav/Navigation.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<!--前端样式1-->
|
||||||
|
<template>
|
||||||
|
<el-row justify="space-between">
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="grid-content ep-bg-purple" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-menu
|
||||||
|
:default-active="nav.frontPath"
|
||||||
|
mode="horizontal"
|
||||||
|
@select="handleSelect"
|
||||||
|
router
|
||||||
|
>
|
||||||
|
<el-menu-item
|
||||||
|
class="nav-name"
|
||||||
|
v-for="r of getFrontList()" :key="r.path"
|
||||||
|
:index="r.path">
|
||||||
|
{{ r.name }}
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="3">
|
||||||
|
<el-button v-if="!userStore().frontIsLogin"
|
||||||
|
style="margin-top: 12px" type="primary"
|
||||||
|
round @click="router.push('/login')">登录</el-button>
|
||||||
|
<el-dropdown v-else>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="24">
|
||||||
|
<h6>{{ state.userInfo.nickName }}</h6>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item @click="to('/user')">个人中心</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="to('/collect')">我的收藏</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="to('/order')">我的换住</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getFrontList } from '~/utils/utils'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { logoutFront } from '~/api/user/frontUserApi'
|
||||||
|
const router = useRouter()
|
||||||
|
const nav = navStore()
|
||||||
|
const handleSelect = (key: string, keyPath: string[]) => {
|
||||||
|
nav.frontPath = key.fullPath
|
||||||
|
}
|
||||||
|
const state = reactive({
|
||||||
|
userInfo:{}
|
||||||
|
})
|
||||||
|
onMounted(()=>{
|
||||||
|
frontRequest.get("/api/user/userInfo").then(response =>{
|
||||||
|
state.userInfo = response.data
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
const logout = () => {
|
||||||
|
logoutFront().then(() => {
|
||||||
|
const user = userStore()
|
||||||
|
user.frontIsLogin = false
|
||||||
|
user.frontToken = ''
|
||||||
|
user.frontUserInfo = {}
|
||||||
|
toast.success('退出成功~')
|
||||||
|
router.push('/login')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function to(path:string) {
|
||||||
|
router.push(path)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
:deep(.el-menu--horizontal) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
h6 {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
padding-top: 15px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.nav-name{
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
</style>
|
100
ui/src/components/view/brand-pie.vue
Normal file
100
ui/src/components/view/brand-pie.vue
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<v-chart class="chart" :option="option" autoresize />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { use } from 'echarts/core'
|
||||||
|
import { PieChart } from 'echarts/charts' // 导入饼图模块
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
|
import {
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
} from 'echarts/components'
|
||||||
|
import VChart from 'vue-echarts'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import axios from 'axios' // 导入 axios
|
||||||
|
|
||||||
|
use([
|
||||||
|
CanvasRenderer,
|
||||||
|
PieChart, // 使用饼图
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
])
|
||||||
|
|
||||||
|
// 定义 option 用来存储饼图配置
|
||||||
|
const option = ref<any>({
|
||||||
|
title: {
|
||||||
|
text: '入住品牌分析',
|
||||||
|
left: 'center',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item', // 饼图的 tooltip 触发方式为 'item'
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: 'vertical', // 图例垂直排列
|
||||||
|
left: 'left',
|
||||||
|
data: [], // 图例数据为日期,初始为空
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '销售数量',
|
||||||
|
type: 'pie', // 设置图表类型为饼图
|
||||||
|
radius: '50%', // 饼图半径
|
||||||
|
data: [], // 饼图数据,初始为空
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取近七天的日期
|
||||||
|
function getLastSevenDays() {
|
||||||
|
const dates = []
|
||||||
|
for (let i = 6; i >= 0; i--) {
|
||||||
|
const date = new Date()
|
||||||
|
date.setDate(date.getDate() - i)
|
||||||
|
const formattedDate = date.toISOString().split('T')[0]
|
||||||
|
dates.push(formattedDate)
|
||||||
|
}
|
||||||
|
return dates
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求数据并更新饼图
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
const response = await adminRequest.get('/sys/item/view1')
|
||||||
|
const data = response.data
|
||||||
|
|
||||||
|
const pieData = data.map((item: { name: string, value: number }) => ({
|
||||||
|
value: item.value,
|
||||||
|
name: item.name,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 更新 option 中的图例数据和饼图数据
|
||||||
|
option.value.legend.data = data
|
||||||
|
option.value.series[0].data = pieData
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据加载失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在组件挂载后请求数据
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
87
ui/src/components/view/foundation-line.vue
Normal file
87
ui/src/components/view/foundation-line.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<v-chart class="chart" :option="option" autoresize />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { use } from 'echarts/core'
|
||||||
|
import { LineChart } from 'echarts/charts'
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
|
import {
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
GridComponent,
|
||||||
|
AxisPointerComponent,
|
||||||
|
} from 'echarts/components'
|
||||||
|
import VChart from 'vue-echarts'
|
||||||
|
use([
|
||||||
|
CanvasRenderer,
|
||||||
|
LineChart,
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
GridComponent,
|
||||||
|
AxisPointerComponent, // 如果需要显示坐标轴指示器,也可以导入
|
||||||
|
])
|
||||||
|
function getLastSevenDays() {
|
||||||
|
const dates = [];
|
||||||
|
for (let i = 6; i >= 0; i--) {
|
||||||
|
const date = new Date();
|
||||||
|
date.setDate(date.getDate() - i);
|
||||||
|
const formattedDate = date.toISOString().split('T')[0];
|
||||||
|
dates.push(formattedDate);
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = ref<any>({
|
||||||
|
title: {
|
||||||
|
text: '近几天房源销售分析',
|
||||||
|
left: 'center',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['数量'],
|
||||||
|
left: 'left',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '10%',
|
||||||
|
right: '10%',
|
||||||
|
bottom: '10%',
|
||||||
|
containLabel: true, // 包含标签
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: getLastSevenDays(),
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '数量',
|
||||||
|
type: 'line', // 设置图表类型为折线图
|
||||||
|
data: [820, 932, 901, 934, 1290, 1330, 1320, 1010, 1100, 1230, 1300, 1420],
|
||||||
|
itemStyle: {
|
||||||
|
color: '#66b3ff',
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
smooth: true, // 平滑曲线
|
||||||
|
areaStyle: { // 添加面积背景色
|
||||||
|
origin: 'start',
|
||||||
|
color: 'rgba(102, 179, 255, 0.2)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
53
ui/src/composables/adminRequest.ts
Normal file
53
ui/src/composables/adminRequest.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export const adminRequest = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_ADMIN_API_BASE_URL,
|
||||||
|
})
|
||||||
|
// 添加请求拦截器
|
||||||
|
adminRequest.interceptors.request.use(
|
||||||
|
function (config) {
|
||||||
|
// 在发送请求之前做些什么
|
||||||
|
const token = userStore().adminToken
|
||||||
|
if (token !== null || token !== undefined) {
|
||||||
|
//添加header
|
||||||
|
config.headers.token = token
|
||||||
|
}
|
||||||
|
// 在发送请求之前做些什么
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
toast.warning(error.message ?? '未知请求错误')
|
||||||
|
// 对请求错误做些什么
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// 添加响应拦截器
|
||||||
|
adminRequest.interceptors.response.use(
|
||||||
|
function (response) {
|
||||||
|
if (response.data.code){
|
||||||
|
const code = response.data.code
|
||||||
|
switch (code) {
|
||||||
|
case 500:
|
||||||
|
toast.error(response.data.msg)
|
||||||
|
return Promise.reject(response.data.msg)
|
||||||
|
case 401:
|
||||||
|
toast.error(response.data.msg)
|
||||||
|
window.open(`/login`, '_self')
|
||||||
|
return Promise.reject(response.data.msg)
|
||||||
|
default:
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
let { msg, message } = error.response?.data ?? {}
|
||||||
|
if (!msg && message) {
|
||||||
|
msg = message
|
||||||
|
}
|
||||||
|
toast.warning(msg)
|
||||||
|
// 超出 2xx 范围的状态码都会触发该函数。
|
||||||
|
// 对响应错误做点什么
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
26
ui/src/composables/env.ts
Normal file
26
ui/src/composables/env.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 是否在开发环境
|
||||||
|
* @description 在 `if (IN_DEV)` 块中的代码在 `vite build` 时会自动编译掉,不会带到生产环境
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* if (IN_DEV) {
|
||||||
|
* console.log("开发环境")
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const IN_DEV = import.meta.env.DEV
|
||||||
|
/**
|
||||||
|
* 是否在生产环境
|
||||||
|
* @example
|
||||||
|
* if (IN_PROD) {
|
||||||
|
* console.log("生产环境")
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const IN_PROD = import.meta.env.PROD
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不带尾部 / 的 BASE_URL
|
||||||
|
*/
|
||||||
|
export const BASE_URL_WITHOUT_TAIL = import.meta.env.BASE_URL.endsWith('/')
|
||||||
|
? import.meta.env.BASE_URL.slice(0, -1)
|
||||||
|
: import.meta.env.BASE_URL
|
76
ui/src/composables/frontRequest.ts
Normal file
76
ui/src/composables/frontRequest.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export const frontRequest = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_FRONT_BASE_URL,
|
||||||
|
})
|
||||||
|
// 添加请求拦截器
|
||||||
|
frontRequest.interceptors.request.use(
|
||||||
|
function (config) {
|
||||||
|
const token = userStore().frontToken
|
||||||
|
|
||||||
|
if (token !== null || token !== undefined) {
|
||||||
|
//添加header
|
||||||
|
config.headers.Authorization = token
|
||||||
|
}
|
||||||
|
// 在发送请求之前做些什么
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
toast.warning(error.message ?? '未知请求错误')
|
||||||
|
// 对请求错误做些什么
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// 添加响应拦截器
|
||||||
|
frontRequest.interceptors.response.use(
|
||||||
|
function (response) {
|
||||||
|
if (response.data.code){
|
||||||
|
const code = response.data.code
|
||||||
|
switch (code) {
|
||||||
|
case 500:
|
||||||
|
toast.error(response.data.msg)
|
||||||
|
return Promise.reject(response.data.msg)
|
||||||
|
case 401:
|
||||||
|
window.open(`/login`, '_self')
|
||||||
|
toast.error("请重新登录~")
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (response.data) {
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
const status = error.response?.status
|
||||||
|
let { msg, message } = error.response?.data ?? {}
|
||||||
|
|
||||||
|
if (!msg && message) {
|
||||||
|
msg = message
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!msg) {
|
||||||
|
switch (status) {
|
||||||
|
case 400:
|
||||||
|
msg = '参数错误'
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
msg = '服务端错误'
|
||||||
|
break
|
||||||
|
case 401:
|
||||||
|
window.location.href = "/dsds"
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
msg = error.message ?? '未知响应错误'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.warning(msg)
|
||||||
|
// 超出 2xx 范围的状态码都会触发该函数。
|
||||||
|
// 对响应错误做点什么
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
7
ui/src/composables/path.ts
Normal file
7
ui/src/composables/path.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* base 安全的路径解析
|
||||||
|
* @param path 路径
|
||||||
|
*/
|
||||||
|
export function safeResolve(path: string) {
|
||||||
|
return BASE_URL_WITHOUT_TAIL + path
|
||||||
|
}
|
3
ui/src/composables/toast.ts
Normal file
3
ui/src/composables/toast.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import 'vue-toastification/dist/index.css'
|
||||||
|
import { createToastInterface } from 'vue-toastification'
|
||||||
|
export default createToastInterface()
|
12
ui/src/composables/useLanguage.ts
Normal file
12
ui/src/composables/useLanguage.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export default () => {
|
||||||
|
const { t, locale } = useI18n()
|
||||||
|
const toggleLocale = () => {
|
||||||
|
locale.value = locale.value === 'zh-CN' ? 'en' : 'zh-CN'
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = computed(() =>
|
||||||
|
locale.value === 'zh-CN' ? '中文' : 'English',
|
||||||
|
)
|
||||||
|
|
||||||
|
return { t, language, toggleLocale }
|
||||||
|
}
|
14
ui/src/composables/useTyped.ts
Normal file
14
ui/src/composables/useTyped.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { init } from 'ityped'
|
||||||
|
|
||||||
|
export default (strings: string[]) => {
|
||||||
|
const typedRef = ref<Element>()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init(typedRef.value!, {
|
||||||
|
strings,
|
||||||
|
showCursor: false,
|
||||||
|
disableBackTyping: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return typedRef
|
||||||
|
}
|
25
ui/src/composables/useVisits.ts
Normal file
25
ui/src/composables/useVisits.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useRequest } from 'vue-request'
|
||||||
|
|
||||||
|
export function useVisits() {
|
||||||
|
// 开发环境下
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
const visits = useStorage('visits-kv', 0)
|
||||||
|
if (typeof visits.value === 'number') {
|
||||||
|
visits.value++
|
||||||
|
}
|
||||||
|
return visits
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: visits } = useRequest(async function () {
|
||||||
|
try {
|
||||||
|
const n = await http.get('https://visits-kv.deno.dev/tov-template', {
|
||||||
|
baseURL: '',
|
||||||
|
})
|
||||||
|
return Number(n) ?? 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return visits ?? 0
|
||||||
|
}
|
59
ui/src/layouts/admin.vue
Normal file
59
ui/src/layouts/admin.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<heads></heads>
|
||||||
|
</el-header>
|
||||||
|
<el-container>
|
||||||
|
<el-aside width="200px" >
|
||||||
|
<el-menu
|
||||||
|
:default-active="state.activeIndex"
|
||||||
|
router
|
||||||
|
@select="handleSelect"
|
||||||
|
>
|
||||||
|
<el-menu-item
|
||||||
|
v-for="r in getAdminList()"
|
||||||
|
:key="r.name"
|
||||||
|
:index="r.path"
|
||||||
|
>
|
||||||
|
<component class="icons" :is="r.icon" />
|
||||||
|
<template #title>{{ r.name }}</template>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
<el-main class="main">
|
||||||
|
<router-view ></router-view>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getAdminList} from '~/utils/utils'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
// 获取当前路由信息
|
||||||
|
const route = useRoute()
|
||||||
|
const state = reactive({
|
||||||
|
activeIndex:'/admin'
|
||||||
|
})
|
||||||
|
const handleSelect = (key: string, keyPath: string[]) => {
|
||||||
|
state.activeIndex = key
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(()=>{
|
||||||
|
// 初始化activeIndex为当前路由的path路径,方便在菜单栏高<EFBFBD><EFBFBD>显示选中项
|
||||||
|
state.activeIndex = route.path
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.main{
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 80px);
|
||||||
|
background-color: #f2f4f8;
|
||||||
|
}
|
||||||
|
.icons{
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
5
ui/src/layouts/default.vue
Normal file
5
ui/src/layouts/default.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
34
ui/src/layouts/front.vue
Normal file
34
ui/src/layouts/front.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
<!--前端模板-->
|
||||||
|
<template>
|
||||||
|
<div class="common-layout">
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<nav-navigation></nav-navigation>
|
||||||
|
</el-header>
|
||||||
|
<el-main class="main">
|
||||||
|
<div class="container">
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
|
||||||
|
.main{
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
background-color: #f0f0f1;
|
||||||
|
}
|
||||||
|
.container{
|
||||||
|
width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
55
ui/src/layouts/frontUserInfo.vue
Normal file
55
ui/src/layouts/frontUserInfo.vue
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<!--前端模板-->
|
||||||
|
<template>
|
||||||
|
<div class="common-layout">
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<nav-navigation></nav-navigation>
|
||||||
|
</el-header>
|
||||||
|
<el-main class="main">
|
||||||
|
<div class="container">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-menu
|
||||||
|
:default-active="state.activeIndex"
|
||||||
|
router
|
||||||
|
>
|
||||||
|
<el-menu-item v-for="item in state.userMenu" :index="item.path">
|
||||||
|
<span>{{item.name}}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="18">
|
||||||
|
<router-view></router-view>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
const state = reactive({
|
||||||
|
activeIndex:'/user',
|
||||||
|
userMenu:[
|
||||||
|
{name: '个人中心', path: '/user'},
|
||||||
|
{name: '我的收藏', path: '/collect'},
|
||||||
|
{name: '我的换住', path: '/order'},
|
||||||
|
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.main{
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
background-color: #f3f3f4;
|
||||||
|
}
|
||||||
|
.container{
|
||||||
|
width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
3
ui/src/layouts/notFound.vue
Normal file
3
ui/src/layouts/notFound.vue
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
17
ui/src/main.ts
Normal file
17
ui/src/main.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// https://unocss.dev/ 原子 css 库
|
||||||
|
import '@unocss/reset/tailwind-compat.css' // unocss reset
|
||||||
|
import 'virtual:uno.css'
|
||||||
|
import 'virtual:unocss-devtools'
|
||||||
|
// 你自定义的 css
|
||||||
|
import './styles/main.css'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
const app = createApp(App)
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
app.use(ElementPlus)
|
||||||
|
|
||||||
|
app.mount('#app')
|
58
ui/src/pages/[...notFound].vue
Normal file
58
ui/src/pages/[...notFound].vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const typedRef = useTyped([' is not found!'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-screen flex flex-wrap items-center justify-around text-center">
|
||||||
|
<div class="desc font-blod">
|
||||||
|
<div class="code text-7xl">404</div>
|
||||||
|
<div ref="typedRef" class="content mb-5 text-3xl">The Page</div>
|
||||||
|
<RouterLink :to="safeResolve('/')">
|
||||||
|
<button
|
||||||
|
class="rounded bg-light-800 px-5 py-2 text-lg transition"
|
||||||
|
hover="shadow-md"
|
||||||
|
dark="text-black"
|
||||||
|
>
|
||||||
|
Go Home
|
||||||
|
</button>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
:src="safeResolve('/notFound/32.svg')"
|
||||||
|
class="cover"
|
||||||
|
alt="page not found"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.code {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
height: auto;
|
||||||
|
width: 700px;
|
||||||
|
margin: 0 5px;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
flex: 1;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"title": "404",
|
||||||
|
"layout": "notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
232
ui/src/pages/address/index.vue
Normal file
232
ui/src/pages/address/index.vue
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!--增、查交易记录的按钮-->
|
||||||
|
<el-button type="primary" round @click="openAddDialog" size="small">添加</el-button>
|
||||||
|
|
||||||
|
<!--表格-->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-table v-loading="loading" :data="state.getList">
|
||||||
|
<!-- 列表结开始-->
|
||||||
|
<el-table-column prop="recipientName" label="收货人" align="center" />
|
||||||
|
<el-table-column prop="phone" label="电话/手机" align="center" />
|
||||||
|
<el-table-column prop="city" label="所在地区" align="center" />
|
||||||
|
<el-table-column prop="address" label="详细地址" align="center" />
|
||||||
|
<!--列表结束-->
|
||||||
|
<el-table-column label="操作" align="center" >
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="del(scope.row.id)" type="danger" size="small">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template v-slot:empty>
|
||||||
|
<el-empty description="数据去外太空了~" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页控件 -->
|
||||||
|
<el-pagination
|
||||||
|
v-if="state.query.total > 0"
|
||||||
|
:current-page="state.query.page"
|
||||||
|
:page-size="state.query.limit"
|
||||||
|
:total="state.query.total"
|
||||||
|
background layout="prev, pager, next"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog v-model="state.dialogVisible" title="新增" width="50%">
|
||||||
|
<el-form
|
||||||
|
:model="state.formData"
|
||||||
|
ref="formRef"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<!-- 表单开始===============================================================================================================================-->
|
||||||
|
<el-form-item label="收货人" prop="recipientName" :rules="[{ required: true, message: '请输入收货人', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="state.formData.recipientName"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="电话/手机" prop="phone" :rules="[{ required: true, message: '请输入电话/手机', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="state.formData.phone"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="所在地区" prop="city" :rules="[{ required: true, message: '请输入所在地区', trigger: 'blur' }]">
|
||||||
|
<!-- <el-input v-model.number="state.formData.city"/>-->
|
||||||
|
<el-cascader
|
||||||
|
style="width: 260px"
|
||||||
|
v-model="value"
|
||||||
|
:options="state.options"
|
||||||
|
:props="props"
|
||||||
|
@change="handleChange"
|
||||||
|
placeholder="请选择地区"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="详细地址" prop="address" :rules="[{ required: true, message: '请输入详细地址', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="state.formData.address"/>
|
||||||
|
</el-form-item>
|
||||||
|
<!--表单结束===============================================================================================================================-->
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="state.dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveTransaction(formRef)">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 记录数据
|
||||||
|
import type {FormInstance, TabsPaneContext} from 'element-plus'
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const loading = ref(true)
|
||||||
|
const state = reactive(<any>{
|
||||||
|
route:"api/address",
|
||||||
|
dialogVisible:false,
|
||||||
|
getList: [],//获取数据
|
||||||
|
query:{
|
||||||
|
total: 0, // 总记录数
|
||||||
|
page: 1, // 当前页码
|
||||||
|
limit: 5, // 每页显示的记录数
|
||||||
|
title:"",
|
||||||
|
status:0,
|
||||||
|
},
|
||||||
|
formData:{},
|
||||||
|
cateList:[],
|
||||||
|
options:[],
|
||||||
|
})
|
||||||
|
const value = ref('')
|
||||||
|
const props = {
|
||||||
|
expandTrigger: 'hover' as const,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转成列表
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
const handleChange = (value:any) => {
|
||||||
|
const labels = getLabelsFromValue(value, state.options);
|
||||||
|
state.formData.city = labels
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归查找 label
|
||||||
|
const getLabelsFromValue = (value:any, options:any) => {
|
||||||
|
const labels:any = [];
|
||||||
|
const findLabels = (value:any, options:any) => {
|
||||||
|
for (const item of options) {
|
||||||
|
if (item.value === value[0]) {
|
||||||
|
labels.push(item.label);
|
||||||
|
if (value.length > 1 && item.children) {
|
||||||
|
findLabels(value.slice(1), item.children);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
findLabels(value, options);
|
||||||
|
return labels;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 获取交易记录
|
||||||
|
const init = () => {
|
||||||
|
frontRequest.get(`${state.route}/page`, {
|
||||||
|
params: state.query
|
||||||
|
}).then((res:any) => {
|
||||||
|
state.getList = res.data.list
|
||||||
|
state.query.total = res.data.total
|
||||||
|
}).finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
// 新增交易记录
|
||||||
|
const openAddDialog = () => {
|
||||||
|
state.formData = {
|
||||||
|
sort:1
|
||||||
|
}
|
||||||
|
state.dialogVisible = true
|
||||||
|
}
|
||||||
|
// 编辑交易记录
|
||||||
|
const edit = (row: any) => {
|
||||||
|
state.formData = row
|
||||||
|
state.dialogVisible = true
|
||||||
|
}
|
||||||
|
// 保存记录
|
||||||
|
const saveTransaction = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
formEl.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
state.formData.city = state.formData.city.toString()
|
||||||
|
if (state.formData.id) {
|
||||||
|
// 更新交易记录
|
||||||
|
frontRequest.put(`${state.route}`, state.formData).then(() =>{
|
||||||
|
init()
|
||||||
|
state.dialogVisible = false
|
||||||
|
ElMessage.success("提交成功~")
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 新增交易记录
|
||||||
|
frontRequest.post(`${state.route}`, state.formData).then(() =>{
|
||||||
|
init()
|
||||||
|
state.dialogVisible = false
|
||||||
|
ElMessage.success("提交成功~")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 删除交易记录
|
||||||
|
const del = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await frontRequest.delete(`${state.route}/${id}`)
|
||||||
|
init()
|
||||||
|
ElMessage.success("删除成功~")
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error("删除失败~")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码改变时的处理函数
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
state.query.page = page
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = (tab: TabsPaneContext, event: Event) => {
|
||||||
|
// 根据 tab 的 name 属性更新状态
|
||||||
|
const selectedStatus = state.getStatus[tab.index].status;
|
||||||
|
state.query.status = selectedStatus;
|
||||||
|
init();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面加载时获取交易记录
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
frontRequest.get(`/api/area/tree`).then((res:any) => {
|
||||||
|
console.log(res)
|
||||||
|
state.options = res.data
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
:deep(.el-upload) {
|
||||||
|
border: 1px dashed var(--el-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: var(--el-transition-duration-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload:hover) {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "front"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
207
ui/src/pages/admin/cate.vue
Normal file
207
ui/src/pages/admin/cate.vue
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form :inline="true" >
|
||||||
|
<el-form-item label="分类名称">
|
||||||
|
<el-input v-model="state.query.name" placeholder="分类名称" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item >
|
||||||
|
<el-button type="primary" @click="init" >查询</el-button>
|
||||||
|
<el-button type="primary" @click="openAddDialog" >添加</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<div style="height: 30px"></div>
|
||||||
|
<!--表格-->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-table :data="state.getList">
|
||||||
|
<!-- 列表结开始-->
|
||||||
|
<el-table-column prop="id" label="编号" align="center" />
|
||||||
|
<el-table-column prop="name" label="分类名称" align="center" />
|
||||||
|
<!-- <el-table-column prop="largePic" label="图片" align="center" >-->
|
||||||
|
<!-- <template #default="{ row }">-->
|
||||||
|
<!-- <!– 点击图片后显示弹框预览 –>-->
|
||||||
|
<!-- <el-image-->
|
||||||
|
<!-- :src="row.path"-->
|
||||||
|
<!-- fit="cover"-->
|
||||||
|
<!-- style="width: 50px;height: 50px"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </template>-->
|
||||||
|
<!-- </el-table-column>-->
|
||||||
|
<!--列表结束-->
|
||||||
|
<el-table-column label="操作" align="center" >
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="edit(scope.row)" size="small">编辑</el-button>
|
||||||
|
<el-button @click="del(scope.row.id)" type="danger" size="small">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template v-slot:empty>
|
||||||
|
<el-empty description="数据去外太空了~" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页控件 -->
|
||||||
|
<el-pagination
|
||||||
|
v-if="state.query.total > 0"
|
||||||
|
:current-page="state.query.page"
|
||||||
|
:page-size="state.query.limit"
|
||||||
|
:total="state.query.total"
|
||||||
|
background layout="prev, pager, next"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog v-model="state.dialogVisible" title="新增" width="50%">
|
||||||
|
<el-form
|
||||||
|
:model="state.formData"
|
||||||
|
ref="formRef"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<!-- 表单开始===============================================================================================================================-->
|
||||||
|
<el-form-item label="分类名称" prop="name" :rules="[{ required: true, message: '请输入分类名称', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="state.formData.name"/>
|
||||||
|
</el-form-item>
|
||||||
|
<!-- <el-form-item label="图片" prop="image" >-->
|
||||||
|
<!-- <image-upload @update:imageUrl="handleImageUrl" :image-url="state.formData.image"></image-upload>-->
|
||||||
|
<!-- </el-form-item>-->
|
||||||
|
|
||||||
|
<!--表单结束===============================================================================================================================-->
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="state.dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveTransaction(formRef)">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 交易记录数据
|
||||||
|
import type { FormInstance } from 'element-plus'
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const state = reactive({
|
||||||
|
route:"sys/categories",
|
||||||
|
dialogVisible:false,
|
||||||
|
getList: [],//获取数据
|
||||||
|
query:{
|
||||||
|
total: 0, // 总记录数
|
||||||
|
page: 1, // 当前页码
|
||||||
|
limit: 10, // 每页显示的记录数
|
||||||
|
},
|
||||||
|
formData:{}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取交易记录
|
||||||
|
const init = () => {
|
||||||
|
adminRequest.get(`${state.route}/page`, {
|
||||||
|
params: state.query
|
||||||
|
}).then((res:any) => {
|
||||||
|
state.getList = res.data.list
|
||||||
|
state.query.total = res.data.total
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 新增交易记录
|
||||||
|
const openAddDialog = () => {
|
||||||
|
// formData = {
|
||||||
|
// name:"",
|
||||||
|
// path:"",
|
||||||
|
// }
|
||||||
|
state.dialogVisible = true
|
||||||
|
}
|
||||||
|
// 编辑交易记录
|
||||||
|
const edit = (row: any) => {
|
||||||
|
state.formData = row
|
||||||
|
state.dialogVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存记录
|
||||||
|
const saveTransaction = (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
formEl.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
if (state.formData.id) {
|
||||||
|
// 更新交易记录
|
||||||
|
adminRequest.put(`${state.route}`, state.formData).then(()=>{
|
||||||
|
init()
|
||||||
|
state.dialogVisible = false
|
||||||
|
ElMessage.success("更新成功~")
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 新增交易记录
|
||||||
|
adminRequest.post(`${state.route}`, state.formData).then(() =>{
|
||||||
|
init()
|
||||||
|
state.dialogVisible = false
|
||||||
|
ElMessage.success("新增成功~")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 删除交易记录
|
||||||
|
const del = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await adminRequest.delete(`${state.route}/${id}`)
|
||||||
|
init()
|
||||||
|
ElMessage.success("删除成功~")
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error("删除失败~")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码改变时的处理函数
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
state.query.page = page
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
// 页面加载时获取交易记录
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
:deep(.el-upload) {
|
||||||
|
border: 1px dashed var(--el-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: var(--el-transition-duration-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload:hover) {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon.avatar-uploader-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.avatar{
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
128
ui/src/pages/admin/comment.vue
Normal file
128
ui/src/pages/admin/comment.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!--增、查交易记录的按钮-->
|
||||||
|
<!-- <el-button type="primary" round @click="openAddDialog" size="small">添加</el-button>-->
|
||||||
|
<!-- <el-row>-->
|
||||||
|
<!-- <el-col :span="24">-->
|
||||||
|
<!-- <el-form :inline="true" :model="state.query" class="demo-form-inline">-->
|
||||||
|
<!-- <el-form-item label="电影名称:">-->
|
||||||
|
<!-- <el-input v-model="state.query.title" placeholder="请输入电影名称" clearable @input="init" />-->
|
||||||
|
<!-- </el-form-item>-->
|
||||||
|
<!-- </el-form>-->
|
||||||
|
<!-- </el-col>-->
|
||||||
|
<!-- </el-row>-->
|
||||||
|
<!--表格-->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-table :data="state.getList">
|
||||||
|
<!-- 列表结开始-->
|
||||||
|
<el-table-column prop="item.id" label="编号" align="center" width="120px"/>
|
||||||
|
<el-table-column prop="item.title" label="名称" align="center" width="120px"/>
|
||||||
|
<el-table-column prop="userEntity.nickName" label="用户昵称" align="center" width="120px" />
|
||||||
|
<el-table-column prop="itemDTO.largePic" label="图片" align="center" width="100" >
|
||||||
|
<template #default="{ row }">
|
||||||
|
<!-- 点击图片后显示弹框预览 -->
|
||||||
|
<el-image
|
||||||
|
:src="row.item?.image"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="score" label="评分" align="center" />
|
||||||
|
<el-table-column prop="content" label="评论内容" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<div style="width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;">
|
||||||
|
{{ scope.row.content }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<template v-slot:empty>
|
||||||
|
<el-empty description="数据去外太空了~" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页控件 -->
|
||||||
|
<el-pagination
|
||||||
|
v-if="state.query.total > 0"
|
||||||
|
:current-page="state.query.page"
|
||||||
|
:page-size="state.query.limit"
|
||||||
|
:total="state.query.total"
|
||||||
|
background layout="prev, pager, next"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
route:"sys/order",
|
||||||
|
dialogVisible:false,
|
||||||
|
getList: [],//获取数据
|
||||||
|
query:{
|
||||||
|
total: 0, // 总记录数
|
||||||
|
page: 1, // 当前页码
|
||||||
|
limit: 10, // 每页显示的记录数
|
||||||
|
title:"",
|
||||||
|
type:"0",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取交易记录
|
||||||
|
const init = () => {
|
||||||
|
adminRequest.get(`${state.route}/page`, {
|
||||||
|
params: state.query
|
||||||
|
}).then((res:any) => {
|
||||||
|
state.getList = res.data.list
|
||||||
|
state.query.total = res.data.total
|
||||||
|
console.log(state.getList)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 页码改变时的处理函数
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
state.query.page = page
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
// 页面加载时获取交易记录
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
:deep(.el-upload) {
|
||||||
|
border: 1px dashed var(--el-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: var(--el-transition-duration-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload:hover) {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon.avatar-uploader-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.avatar{
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
391
ui/src/pages/admin/house.vue
Normal file
391
ui/src/pages/admin/house.vue
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
<template>
|
||||||
|
<div class="house-management-container">
|
||||||
|
<!-- 搜索区域 -->
|
||||||
|
<div class="search-area">
|
||||||
|
<el-form :inline="true" :model="state.query" class="search-form">
|
||||||
|
<el-form-item label="房源名称:" class="search-item">
|
||||||
|
<el-input
|
||||||
|
v-model="state.query.title"
|
||||||
|
placeholder="请输入房源名称"
|
||||||
|
clearable
|
||||||
|
@input="handleSearch"
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态:" class="search-item">
|
||||||
|
<el-select clearable
|
||||||
|
v-model="state.query.status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
@change="handleSearch"
|
||||||
|
style="width: 120px">
|
||||||
|
<el-option
|
||||||
|
v-for="item in state.statusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item class="action-buttons">
|
||||||
|
<el-button type="primary" @click="init" class="query-button">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
<span>查询</span>
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="openAddDialog" class="add-button">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
<span>添加</span>
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<!-- 表格区域 -->
|
||||||
|
<div class="table-area">
|
||||||
|
<el-table
|
||||||
|
v-loading="state.loading"
|
||||||
|
:data="state.list"
|
||||||
|
class="data-table"
|
||||||
|
empty-text="暂无数据"
|
||||||
|
>
|
||||||
|
<el-table-column label="封面图" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-image
|
||||||
|
:src="row.image"
|
||||||
|
fit="cover"
|
||||||
|
class="house-image"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="title" label="标题" min-width="120" align="center"/>
|
||||||
|
<el-table-column prop="name" label="小区名称" min-width="120" align="center"/>
|
||||||
|
<el-table-column prop="city" label="城市" width="100" align="center"/>
|
||||||
|
<el-table-column prop="type" label="户型" width="120" align="center"/>
|
||||||
|
<el-table-column prop="area" label="面积" width="100" align="center"/>
|
||||||
|
<el-table-column prop="floor" label="楼层" width="80" align="center"/>
|
||||||
|
<el-table-column prop="face" label="朝向" width="100" align="center"/>
|
||||||
|
<el-table-column prop="decoration" label="装修" width="100" align="center"/>
|
||||||
|
<el-table-column prop="price" label="价格 (元/㎡)" width="120" align="center"/>
|
||||||
|
<el-table-column prop="status" label="状态" min-width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusType(row.status)">
|
||||||
|
{{ getStatusName(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" fixed="right" width="180" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" type="primary" plain @click="edit(row)">编辑</el-button>
|
||||||
|
<el-popconfirm title="确认删除该房源?" @confirm="del(row.id)">
|
||||||
|
<template #reference>
|
||||||
|
<el-button v-if="row.status != 1" size="small" type="danger" plain>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container" v-if="state.query.total > 0">
|
||||||
|
<el-pagination
|
||||||
|
:current-page="state.query.page"
|
||||||
|
:page-size="state.query.limit"
|
||||||
|
:total="state.query.total"
|
||||||
|
:page-sizes="[5,10,20,50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
background
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 新增/编辑 对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="state.dialogVisible"
|
||||||
|
:title="state.formData.id ? '编辑房源' : '新增房源'"
|
||||||
|
width="700px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
:model="state.formData"
|
||||||
|
ref="formRef"
|
||||||
|
label-width="120px"
|
||||||
|
label-position="right"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户" prop="userId" :rules="[{ required: true, message: '请选择用户', trigger: 'blur' }]">
|
||||||
|
<el-select v-model="state.formData.userId" placeholder="请选择用户" filterable clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="user in state.userList"
|
||||||
|
:key="user.id"
|
||||||
|
:label="`${user.username}(姓名: ${user.nickName})`"
|
||||||
|
:value="user.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标题" prop="title" :rules="[{ required: true, message: '请输入标题', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="state.formData.title" placeholder="请输入标题" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<el-form-item label="小区名称" prop="name" :rules="[{ required: true, message: '请输入小区名称', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="state.formData.name" placeholder="请输入小区名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="城市" prop="city" :rules="[{ required: true, message: '请输入城市', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="state.formData.city" placeholder="请输入城市" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="户型" prop="type" :rules="[{ required: true, message: '请输入户型', trigger: 'blur' }]">
|
||||||
|
<el-select v-model="state.formData.type" placeholder="请选择户型" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="item in getHouseType()"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="面积(㎡)" prop="area">
|
||||||
|
<el-input v-model="state.formData.area" placeholder="请输入面积" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="楼层" prop="floor">
|
||||||
|
<el-input-number v-model="state.formData.floor" :min="0" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="朝向" prop="face">
|
||||||
|
<el-select v-model="state.formData.face" placeholder="请选择朝向">
|
||||||
|
<el-option v-for="item in getFace()" :label="item" :value="item" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="装修" prop="decoration">
|
||||||
|
<el-select v-model="state.formData.decoration" placeholder="请选择装修">
|
||||||
|
<el-option v-for="item in getDecoration()" :label="item" :value="item" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="封面图" prop="image" :rules="[{ required: true, message: '请上传封面图', trigger: 'blur' }]">
|
||||||
|
<image-upload
|
||||||
|
class="image-uploader"
|
||||||
|
:image-url="state.formData.image"
|
||||||
|
@update:imageUrl="handleImageUrl"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="价格(元/㎡)" prop="price">
|
||||||
|
<el-input-number v-model="state.formData.price" :min="0" :step="0.01" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标签" prop="tag">
|
||||||
|
<el-input v-model="state.formData.tag" placeholder="请输入标签,用逗号分隔" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序" prop="sort">
|
||||||
|
<el-input-number v-model="state.formData.sort" :min="1" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="描述" prop="description">
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
v-model="state.formData.description"
|
||||||
|
rows="3"
|
||||||
|
placeholder="请输入描述"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="state.formData.status" placeholder="请选择状态">
|
||||||
|
<el-option
|
||||||
|
v-for="item in getHouseStatus()"
|
||||||
|
:key="item.status"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.status"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="closeDialog">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveTransaction">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import type { FormInstance } from 'element-plus'
|
||||||
|
import { Search, Plus, Picture } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { getDecoration, getFace, getHouseStatus, getHouseType, getStatusName, getStatusType } from '~/utils/utils'
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const state = reactive({
|
||||||
|
route: 'sys/house',
|
||||||
|
loading: false,
|
||||||
|
list: [] as any[],
|
||||||
|
dialogVisible: false,
|
||||||
|
// 搜索 / 分页
|
||||||
|
query: {
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
limit: 5,
|
||||||
|
title: '',
|
||||||
|
status: null as number | null,
|
||||||
|
},
|
||||||
|
// 下拉状态选项
|
||||||
|
statusOptions: [
|
||||||
|
{ label: '未审核', value: 0 },
|
||||||
|
{ label: '上架', value: 1 },
|
||||||
|
{ label: '下架', value: 2 },
|
||||||
|
{ label: '审核失败', value: 3 },
|
||||||
|
],
|
||||||
|
// 表单数据
|
||||||
|
formData: {} as Record<string, any>,
|
||||||
|
userList:[]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 拉取列表
|
||||||
|
const init = () => {
|
||||||
|
state.loading = true
|
||||||
|
adminRequest
|
||||||
|
.get(`${state.route}/page`, { params: state.query })
|
||||||
|
.then((res: any) => {
|
||||||
|
state.list = res.data.list
|
||||||
|
state.query.total = res.data.total
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
state.loading = false
|
||||||
|
})
|
||||||
|
|
||||||
|
adminRequest.get(`sys/user-front/page`, {
|
||||||
|
params: { limit:999 }
|
||||||
|
}).then((res:any) => {
|
||||||
|
state.userList = res.data.list
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索重置页码
|
||||||
|
const handleSearch = () => {
|
||||||
|
state.query.page = 1
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开新增
|
||||||
|
const openAddDialog = () => {
|
||||||
|
state.formData = {
|
||||||
|
title: '',
|
||||||
|
name: '',
|
||||||
|
city: '',
|
||||||
|
type: '',
|
||||||
|
area: '',
|
||||||
|
floor: 1,
|
||||||
|
face: '',
|
||||||
|
decoration: '',
|
||||||
|
image: '',
|
||||||
|
price: 0,
|
||||||
|
description: '',
|
||||||
|
tag: '',
|
||||||
|
hex: '',
|
||||||
|
sort: 1,
|
||||||
|
status: 0,
|
||||||
|
view: 0,
|
||||||
|
createTime: '',
|
||||||
|
userId: null
|
||||||
|
}
|
||||||
|
state.dialogVisible = true
|
||||||
|
nextTick(() => formRef.value?.clearValidate())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭对话框
|
||||||
|
const closeDialog = () => {
|
||||||
|
state.dialogVisible = false
|
||||||
|
state.formData = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
const edit = (row: any) => {
|
||||||
|
state.formData = { ...row }
|
||||||
|
state.dialogVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存(新增/更新)
|
||||||
|
const saveTransaction = () => {
|
||||||
|
formRef.value?.validate(async valid => {
|
||||||
|
if (!valid) return
|
||||||
|
try {
|
||||||
|
const req = state.formData.id
|
||||||
|
? adminRequest.put(`${state.route}`, state.formData)
|
||||||
|
: adminRequest.post(`${state.route}`, state.formData)
|
||||||
|
await req
|
||||||
|
ElMessage.success('操作成功')
|
||||||
|
closeDialog()
|
||||||
|
init()
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('操作失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const del = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await adminRequest.delete(`${state.route}/${id}`)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
init()
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
state.query.page = page
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
state.query.limit = size
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
// 处理图片上传回调
|
||||||
|
const handleImageUrl = (url: string) => {
|
||||||
|
state.formData.image = url
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.house-management-container {
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.search-area {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.search-item { margin-right: 16px; }
|
||||||
|
.search-input { width: 220px; }
|
||||||
|
.action-buttons { margin-left: auto; display: flex; gap: 8px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.table-area {
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
.house-image {
|
||||||
|
width: 60px; height: 60px; border-radius: 4px;
|
||||||
|
&:hover { transform: scale(1.05); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex; justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.form-dialog {
|
||||||
|
:deep(.el-dialog__body) { padding: 20px 30px; }
|
||||||
|
.image-uploader { width: 100%; }
|
||||||
|
.dialog-footer { text-align: center; padding-top: 16px; border-top: 1px solid #eee; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "admin",
|
||||||
|
"title": "房源管理"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
378
ui/src/pages/admin/index.vue
Normal file
378
ui/src/pages/admin/index.vue
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-container">
|
||||||
|
<!-- 顶部快捷导航 -->
|
||||||
|
<div class="quick-access">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" class="quick-card" @click="navigateTo('house')">
|
||||||
|
<div class="quick-content">
|
||||||
|
<el-icon :size="30" color="#409EFF"><House /></el-icon>
|
||||||
|
<span>房源管理</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" class="quick-card" @click="navigateTo('house')">
|
||||||
|
<div class="quick-content">
|
||||||
|
<el-icon :size="30" color="#E6A23C"><DocumentChecked /></el-icon>
|
||||||
|
<span>认证管理</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" class="quick-card" @click="navigateTo('user')">
|
||||||
|
<div class="quick-content">
|
||||||
|
<el-icon :size="30" color="#F56C6C"><User /></el-icon>
|
||||||
|
<span>用户管理</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
<!-- 数据概览 -->
|
||||||
|
<el-row :gutter="20" class="mt-20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>系统数据概览</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="data-overview">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="data-item">
|
||||||
|
<div class="data-value">{{ overviewData.totalProperties }}</div>
|
||||||
|
<div class="data-label">总房源</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="data-item">
|
||||||
|
<div class="data-value">{{ overviewData.activeUsers }}</div>
|
||||||
|
<div class="data-label">活跃用户</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="data-item">
|
||||||
|
<div class="data-value">{{ overviewData.totalTransactions }}</div>
|
||||||
|
<div class="data-label">总交易</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20" class="mt-20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="data-item">
|
||||||
|
<div class="data-value">{{ overviewData.pendingCertifications }}</div>
|
||||||
|
<div class="data-label">待认证</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="data-item">
|
||||||
|
<div class="data-value">{{ overviewData.availableProperties }}</div>
|
||||||
|
<div class="data-label">可换住房源</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="data-item">
|
||||||
|
<div class="data-value">{{ overviewData.blockHeight }}</div>
|
||||||
|
<div class="data-label">区块链高度</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>交易趋势</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="chart" style="height: 220px;"></div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import {
|
||||||
|
House,
|
||||||
|
Money,
|
||||||
|
DocumentChecked,
|
||||||
|
User
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 数据概览
|
||||||
|
const overviewData = ref({
|
||||||
|
totalProperties: 1245,
|
||||||
|
activeUsers: 876,
|
||||||
|
totalTransactions: 3421,
|
||||||
|
pendingCertifications: 23,
|
||||||
|
availableProperties: 567,
|
||||||
|
blockHeight: 124567
|
||||||
|
})
|
||||||
|
|
||||||
|
// 最近交易
|
||||||
|
const recentTransactions = ref([
|
||||||
|
{
|
||||||
|
txHash: '0x4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6',
|
||||||
|
property: '阳光公寓-302',
|
||||||
|
fromUser: '用户A',
|
||||||
|
toUser: '用户B',
|
||||||
|
date: '2023-05-15',
|
||||||
|
status: '已完成'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
txHash: '0x5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7',
|
||||||
|
property: '绿洲小区-1201',
|
||||||
|
fromUser: '用户C',
|
||||||
|
toUser: '用户D',
|
||||||
|
date: '2023-05-14',
|
||||||
|
status: '进行中'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
txHash: '0x6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8',
|
||||||
|
property: '湖畔别墅-8',
|
||||||
|
fromUser: '用户E',
|
||||||
|
toUser: '用户F',
|
||||||
|
date: '2023-05-13',
|
||||||
|
status: '已完成'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
txHash: '0x7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9',
|
||||||
|
property: '城市花园-502',
|
||||||
|
fromUser: '用户G',
|
||||||
|
toUser: '用户H',
|
||||||
|
date: '2023-05-12',
|
||||||
|
status: '已取消'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
txHash: '0x8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0',
|
||||||
|
property: '星空大厦-2103',
|
||||||
|
fromUser: '用户I',
|
||||||
|
toUser: '用户J',
|
||||||
|
date: '2023-05-11',
|
||||||
|
status: '已完成'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 待认证申请
|
||||||
|
const pendingCertifications = ref([
|
||||||
|
{
|
||||||
|
id: 1001,
|
||||||
|
user: '用户K',
|
||||||
|
type: '身份认证',
|
||||||
|
submitTime: '2023-05-15 14:30'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1002,
|
||||||
|
user: '用户L',
|
||||||
|
type: '房源认证',
|
||||||
|
submitTime: '2023-05-15 13:45'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1003,
|
||||||
|
user: '用户M',
|
||||||
|
type: '身份认证',
|
||||||
|
submitTime: '2023-05-15 11:20'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1004,
|
||||||
|
user: '用户N',
|
||||||
|
type: '房源认证',
|
||||||
|
submitTime: '2023-05-15 10:15'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1005,
|
||||||
|
user: '用户O',
|
||||||
|
type: '身份认证',
|
||||||
|
submitTime: '2023-05-14 18:40'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 区块链信息
|
||||||
|
const blockchainInfo = ref({
|
||||||
|
height: 124567,
|
||||||
|
nodes: 12,
|
||||||
|
pendingTxs: 8,
|
||||||
|
status: '运行正常'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图表引用
|
||||||
|
const chart = ref(null)
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
onMounted(() => {
|
||||||
|
if (chart.value) {
|
||||||
|
const myChart = echarts.init(chart.value)
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['换住交易', '认证交易']
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月']
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '换住交易',
|
||||||
|
type: 'bar',
|
||||||
|
data: [120, 132, 101, 134, 290, 230, 210]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '认证交易',
|
||||||
|
type: 'bar',
|
||||||
|
data: [220, 182, 191, 234, 290, 330, 310]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
myChart.setOption(option)
|
||||||
|
|
||||||
|
// 响应式调整
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
myChart.resize()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 工具方法
|
||||||
|
const shortenHash = (hash: string) => {
|
||||||
|
return hash ? `${hash.substring(0, 6)}...${hash.substring(hash.length - 4)}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusTagType = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case '已完成':
|
||||||
|
return 'success'
|
||||||
|
case '进行中':
|
||||||
|
return 'primary'
|
||||||
|
case '已取消':
|
||||||
|
return 'danger'
|
||||||
|
default:
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航方法
|
||||||
|
const navigateTo = (path: string) => {
|
||||||
|
router.push(`/admin/${path}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 认证处理
|
||||||
|
const handleCertify = (id: number, action: string) => {
|
||||||
|
console.log(`处理认证 ${id},操作:${action}`)
|
||||||
|
// 实际应用中这里会调用API
|
||||||
|
ElMessage.success(`认证ID ${id} 已${action === 'approve' ? '通过' : '拒绝'}`)
|
||||||
|
|
||||||
|
// 从待认证列表中移除
|
||||||
|
pendingCertifications.value = pendingCertifications.value.filter(item => item.id !== id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-access {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-card {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-content span {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-overview {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-20 {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-ellipsis {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blockchain-info {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "admin",
|
||||||
|
"title": "区块链屋屋换住系统 - 后台首页"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
211
ui/src/pages/admin/leaderboard.vue
Normal file
211
ui/src/pages/admin/leaderboard.vue
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!--增、查交易记录的按钮-->
|
||||||
|
<el-button type="primary" round @click="openAddDialog" size="small">添加</el-button>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form :inline="true" :model="state.query" class="demo-form-inline">
|
||||||
|
<el-form-item label="文创名称:">
|
||||||
|
<el-input v-model="state.query.title" placeholder="请输入文创名称" clearable @input="init" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<!--表格-->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-table :data="state.getList">
|
||||||
|
<!-- 列表结开始-->
|
||||||
|
<el-table-column prop="id" label="编号" align="center" />
|
||||||
|
<el-table-column prop="title" label="文创名称" align="center" />
|
||||||
|
|
||||||
|
<el-table-column prop="view" label="阅读量" align="center" />
|
||||||
|
<el-table-column prop="sort" label="排序" align="center" />
|
||||||
|
<!--列表结束-->
|
||||||
|
<el-table-column label="操作" align="center" >
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="info(scope.row.content)" size="small">详情</el-button>
|
||||||
|
<el-button @click="edit(scope.row)" size="small">编辑</el-button>
|
||||||
|
<el-button @click="del(scope.row.id)" type="danger" size="small">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template v-slot:empty>
|
||||||
|
<el-empty description="数据去外太空了~" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页控件 -->
|
||||||
|
<el-pagination
|
||||||
|
v-if="state.query.total > 0"
|
||||||
|
:current-page="state.query.page"
|
||||||
|
:page-size="state.query.limit"
|
||||||
|
:total="state.query.total"
|
||||||
|
background layout="prev, pager, next"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog v-model="state.dialogVisible" title="新增" width="50%">
|
||||||
|
<el-form
|
||||||
|
:model="state.formData"
|
||||||
|
ref="formRef"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 表单开始===============================================================================================================================-->
|
||||||
|
<el-form-item label="文创名称" prop="title" :rules="[{ required: true, message: '请输入文创名称', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="state.formData.title"/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="内容" prop="content" :rules="[{ required: true, message: '请编写内容', trigger: 'blur' }]">
|
||||||
|
<e-editor :content="state.formData.content" @update:content="handleImageUrl" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序" prop="sort" :rules="[{ required: true, message: '请输入排序', trigger: 'blur' }]">
|
||||||
|
<el-input-number v-model="state.formData.sort" :min="1" :max="10000" />
|
||||||
|
</el-form-item>
|
||||||
|
<!--表单结束===============================================================================================================================-->
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="state.dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveTransaction(formRef)">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="详情"
|
||||||
|
width="500"
|
||||||
|
>
|
||||||
|
<div v-html="state.info"></div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 交易记录数据
|
||||||
|
import type { FormInstance } from 'element-plus'
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const state = reactive({
|
||||||
|
info:"",
|
||||||
|
route:"sys/topic",
|
||||||
|
dialogVisible:false,
|
||||||
|
getList: [],//获取数据
|
||||||
|
query:{
|
||||||
|
total: 0, // 总记录数
|
||||||
|
page: 1, // 当前页码
|
||||||
|
limit: 10, // 每页显示的记录数
|
||||||
|
title:"",
|
||||||
|
type:1,
|
||||||
|
},
|
||||||
|
formData:{}
|
||||||
|
})
|
||||||
|
const info = (info:string) => {
|
||||||
|
console.log(info)
|
||||||
|
state.info = info
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取交易记录
|
||||||
|
const init = () => {
|
||||||
|
adminRequest.get(`${state.route}/page`, {
|
||||||
|
params: state.query
|
||||||
|
}).then((res:any) => {
|
||||||
|
state.getList = res.data.list
|
||||||
|
state.query.total = res.data.total
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 新增交易记录
|
||||||
|
const openAddDialog = () => {
|
||||||
|
state.formData = {}
|
||||||
|
state.dialogVisible = true
|
||||||
|
}
|
||||||
|
// 编辑交易记录
|
||||||
|
const edit = (row: any) => {
|
||||||
|
state.formData = row
|
||||||
|
state.dialogVisible = true
|
||||||
|
}
|
||||||
|
// 接收子组件传递的路径
|
||||||
|
function handleImageUrl(content: string) {
|
||||||
|
state.formData.content = content
|
||||||
|
}
|
||||||
|
// 保存记录
|
||||||
|
const saveTransaction = (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
formEl.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
state.formData.type = 1
|
||||||
|
if (state.formData.id) {
|
||||||
|
// 更新交易记录
|
||||||
|
adminRequest.put(`${state.route}`, state.formData)
|
||||||
|
console.log(state.formData)
|
||||||
|
} else {
|
||||||
|
// 新增交易记录
|
||||||
|
adminRequest.post(`${state.route}`, state.formData)
|
||||||
|
}
|
||||||
|
init()
|
||||||
|
state.dialogVisible = false
|
||||||
|
ElMessage.success("提交成功~")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 删除交易记录
|
||||||
|
const del = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await adminRequest.delete(`${state.route}/${id}`)
|
||||||
|
init()
|
||||||
|
ElMessage.success("删除成功~")
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error("删除失败~")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码改变时的处理函数
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
state.query.page = page
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
// 页面加载时获取交易记录
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
:deep(.el-upload) {
|
||||||
|
border: 1px dashed var(--el-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: var(--el-transition-duration-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload:hover) {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon.avatar-uploader-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.avatar{
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
94
ui/src/pages/admin/order.vue
Normal file
94
ui/src/pages/admin/order.vue
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<!--用户详情-->
|
||||||
|
<template>
|
||||||
|
<el-tabs v-model="state.page.status" @tab-click="handleClick">
|
||||||
|
<el-tab-pane v-for="status in state.getStatus" :label="status.name" :name="status.status">
|
||||||
|
<el-table
|
||||||
|
:data="state.getList"
|
||||||
|
style="width: 100%"
|
||||||
|
class="order-table"
|
||||||
|
>
|
||||||
|
<el-table-column prop="item.name" label="换入方房源名称" align="center"/>
|
||||||
|
<el-table-column label="换入方房源图片" >
|
||||||
|
<template #default="{ row }">
|
||||||
|
<img :src="row.item.image" class="product-image">
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="item.name" label="换出方房源名称" align="center"/>
|
||||||
|
<el-table-column label="换出方房源图片" >
|
||||||
|
<template #default="{ row }">
|
||||||
|
<img :src="row.itemA.image" class="product-image">
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="itemA.price" label="状态" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.status }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template v-slot:empty>
|
||||||
|
<el-empty description="数据去外太空了~" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
import { TabsPaneContext } from 'element-plus'
|
||||||
|
const state = reactive({
|
||||||
|
getStatus: [
|
||||||
|
{name: "全部", status:null},
|
||||||
|
{name: "待审核", status: "待审核"},
|
||||||
|
{name: "已同意", status: "已同意"},
|
||||||
|
{name: "已拒绝 ", status: "已拒绝 "}
|
||||||
|
],
|
||||||
|
getList:[],
|
||||||
|
orderForm:{
|
||||||
|
score: 0
|
||||||
|
},
|
||||||
|
page:{
|
||||||
|
page:1,
|
||||||
|
limit:10,
|
||||||
|
order:'desc',
|
||||||
|
status:null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* 获取列表数据
|
||||||
|
*/
|
||||||
|
function getList() {
|
||||||
|
adminRequest.get("/sys/order/page",{ params: state.page}).then(res =>{
|
||||||
|
state.getList = res.data.list;
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const handleClick = (tab: TabsPaneContext, event: Event) => {
|
||||||
|
const selectedStatus = state.getStatus[tab.index].status;
|
||||||
|
state.page.status = selectedStatus;
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(()=>{
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.product-image {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
203
ui/src/pages/admin/slides.vue
Normal file
203
ui/src/pages/admin/slides.vue
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!--增、查交易记录的按钮-->
|
||||||
|
<el-button type="primary" round @click="openAddDialog" size="small">添加</el-button>
|
||||||
|
<!--表格-->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-table :data="state.getList">
|
||||||
|
<!-- 列表结开始-->
|
||||||
|
<el-table-column prop="id" label="编号" align="center" />
|
||||||
|
<el-table-column prop="name" label="轮播名称" align="center" />
|
||||||
|
<el-table-column prop="largePic" label="图片" align="center" >
|
||||||
|
<template #default="{ row }">
|
||||||
|
<!-- 点击图片后显示弹框预览 -->
|
||||||
|
<el-image
|
||||||
|
:src="row.path"
|
||||||
|
fit="cover"
|
||||||
|
style="width: 200px;height: 100px"
|
||||||
|
:preview-src-list="[row.path]"
|
||||||
|
:preview-teleported="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<!--列表结束-->
|
||||||
|
<el-table-column label="操作" align="center" >
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="edit(scope.row)" size="small">编辑</el-button>
|
||||||
|
<el-button @click="del(scope.row.id)" type="danger" size="small">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template v-slot:empty>
|
||||||
|
<el-empty description="数据去外太空了~" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页控件 -->
|
||||||
|
<el-pagination
|
||||||
|
v-if="state.query.total > 0"
|
||||||
|
:current-page="state.query.page"
|
||||||
|
:page-size="state.query.limit"
|
||||||
|
:total="state.query.total"
|
||||||
|
background layout="prev, pager, next"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog v-model="state.dialogVisible" title="新增" width="50%">
|
||||||
|
<el-form
|
||||||
|
:model="state.formData"
|
||||||
|
ref="formRef"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<!-- 表单开始===============================================================================================================================-->
|
||||||
|
|
||||||
|
<el-form-item label="轮播名称" prop="name" :rules="[{ required: true, message: '请输入轮播名称', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="state.formData.name"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="图片" prop="path" :rules="[{ required: true, message: '请插入图片', trigger: 'blur' }]">
|
||||||
|
<image-upload @update:imageUrl="handleImageUrl" :image-url="state.formData.path"></image-upload>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!--表单结束===============================================================================================================================-->
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="state.dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveTransaction(formRef)">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 交易记录数据
|
||||||
|
import type { FormInstance } from 'element-plus'
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const state = reactive({
|
||||||
|
route:"sys/slides",
|
||||||
|
dialogVisible:false,
|
||||||
|
getList: [],//获取数据
|
||||||
|
query:{
|
||||||
|
total: 0, // 总记录数
|
||||||
|
page: 1, // 当前页码
|
||||||
|
limit: 10, // 每页显示的记录数
|
||||||
|
},
|
||||||
|
formData:{}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 接收子组件传递的路径
|
||||||
|
function handleImageUrl(path: string) {
|
||||||
|
state.formData.path = path
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取交易记录
|
||||||
|
const init = () => {
|
||||||
|
adminRequest.get(`${state.route}/page`, {
|
||||||
|
params: state.query
|
||||||
|
}).then((res:any) => {
|
||||||
|
state.getList = res.data.list
|
||||||
|
state.query.total = res.data.total
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 新增交易记录
|
||||||
|
const openAddDialog = () => {
|
||||||
|
// formData = {
|
||||||
|
// name:"",
|
||||||
|
// path:"",
|
||||||
|
// }
|
||||||
|
state.dialogVisible = true
|
||||||
|
}
|
||||||
|
// 编辑交易记录
|
||||||
|
const edit = (row: any) => {
|
||||||
|
state.formData = row
|
||||||
|
state.dialogVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存记录
|
||||||
|
const saveTransaction = (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
formEl.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
if (state.formData.id) {
|
||||||
|
// 更新交易记录
|
||||||
|
adminRequest.put(`${state.route}`, state.formData).then(()=>{
|
||||||
|
init()
|
||||||
|
state.dialogVisible = false
|
||||||
|
ElMessage.success("提交成功~")
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 新增交易记录
|
||||||
|
adminRequest.post(`${state.route}`, state.formData).then(() =>{
|
||||||
|
init()
|
||||||
|
state.dialogVisible = false
|
||||||
|
ElMessage.success("提交成功~")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 删除交易记录
|
||||||
|
const del = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await adminRequest.delete(`${state.route}/${id}`)
|
||||||
|
init()
|
||||||
|
ElMessage.success("删除成功~")
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error("删除失败~")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码改变时的处理函数
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
state.query.page = page
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
// 页面加载时获取交易记录
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
:deep(.el-upload) {
|
||||||
|
border: 1px dashed var(--el-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: var(--el-transition-duration-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload:hover) {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon.avatar-uploader-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.avatar{
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
211
ui/src/pages/admin/topic.vue
Normal file
211
ui/src/pages/admin/topic.vue
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!--增、查交易记录的按钮-->
|
||||||
|
<el-button type="primary" round @click="openAddDialog" size="small">添加</el-button>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form :inline="true" :model="state.query" class="demo-form-inline">
|
||||||
|
<el-form-item label="话题名称:">
|
||||||
|
<el-input v-model="state.query.title" placeholder="请输入话题名称" clearable @input="init" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<!--表格-->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-table :data="state.getList">
|
||||||
|
<!-- 列表结开始-->
|
||||||
|
<el-table-column prop="id" label="编号" align="center" />
|
||||||
|
<el-table-column prop="title" label="话题名称" align="center" />
|
||||||
|
|
||||||
|
<el-table-column prop="view" label="阅读量" align="center" />
|
||||||
|
<el-table-column prop="sort" label="排序" align="center" />
|
||||||
|
<!--列表结束-->
|
||||||
|
<el-table-column label="操作" align="center" >
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="info(scope.row.content)" size="small">详情</el-button>
|
||||||
|
<el-button @click="edit(scope.row)" size="small">编辑</el-button>
|
||||||
|
<el-button @click="del(scope.row.id)" type="danger" size="small">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template v-slot:empty>
|
||||||
|
<el-empty description="数据去外太空了~" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页控件 -->
|
||||||
|
<el-pagination
|
||||||
|
v-if="state.query.total > 0"
|
||||||
|
:current-page="state.query.page"
|
||||||
|
:page-size="state.query.limit"
|
||||||
|
:total="state.query.total"
|
||||||
|
background layout="prev, pager, next"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog v-model="state.dialogVisible" title="新增" width="50%">
|
||||||
|
<el-form
|
||||||
|
:model="state.formData"
|
||||||
|
ref="formRef"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 表单开始===============================================================================================================================-->
|
||||||
|
<el-form-item label="话题名称" prop="title" :rules="[{ required: true, message: '请输入话题名称', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="state.formData.title"/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="内容" prop="content" :rules="[{ required: true, message: '请编写内容', trigger: 'blur' }]">
|
||||||
|
<e-editor :content="state.formData.content" @update:content="handleImageUrl" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序" prop="sort" :rules="[{ required: true, message: '请输入排序', trigger: 'blur' }]">
|
||||||
|
<el-input-number v-model="state.formData.sort" :min="1" :max="10000" />
|
||||||
|
</el-form-item>
|
||||||
|
<!--表单结束===============================================================================================================================-->
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="state.dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveTransaction(formRef)">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="详情"
|
||||||
|
width="500"
|
||||||
|
>
|
||||||
|
<div v-html="state.info"></div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 交易记录数据
|
||||||
|
import type { FormInstance } from 'element-plus'
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const state = reactive({
|
||||||
|
info:"",
|
||||||
|
route:"sys/topic",
|
||||||
|
dialogVisible:false,
|
||||||
|
getList: [],//获取数据
|
||||||
|
query:{
|
||||||
|
total: 0, // 总记录数
|
||||||
|
page: 1, // 当前页码
|
||||||
|
limit: 10, // 每页显示的记录数
|
||||||
|
title:"",
|
||||||
|
type:0,
|
||||||
|
},
|
||||||
|
formData:{}
|
||||||
|
})
|
||||||
|
const info = (info:string) => {
|
||||||
|
console.log(info)
|
||||||
|
state.info = info
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取交易记录
|
||||||
|
const init = () => {
|
||||||
|
adminRequest.get(`${state.route}/page`, {
|
||||||
|
params: state.query
|
||||||
|
}).then((res:any) => {
|
||||||
|
state.getList = res.data.list
|
||||||
|
state.query.total = res.data.total
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 新增交易记录
|
||||||
|
const openAddDialog = () => {
|
||||||
|
state.formData = {}
|
||||||
|
state.dialogVisible = true
|
||||||
|
}
|
||||||
|
// 编辑交易记录
|
||||||
|
const edit = (row: any) => {
|
||||||
|
state.formData = row
|
||||||
|
state.dialogVisible = true
|
||||||
|
}
|
||||||
|
// 接收子组件传递的路径
|
||||||
|
function handleImageUrl(content: string) {
|
||||||
|
state.formData.content = content
|
||||||
|
}
|
||||||
|
// 保存记录
|
||||||
|
const saveTransaction = (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
formEl.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
state.formData.type = 0
|
||||||
|
if (state.formData.id) {
|
||||||
|
// 更新交易记录
|
||||||
|
adminRequest.put(`${state.route}`, state.formData)
|
||||||
|
console.log(state.formData)
|
||||||
|
} else {
|
||||||
|
// 新增交易记录
|
||||||
|
adminRequest.post(`${state.route}`, state.formData)
|
||||||
|
}
|
||||||
|
init()
|
||||||
|
state.dialogVisible = false
|
||||||
|
ElMessage.success("提交成功~")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 删除交易记录
|
||||||
|
const del = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await adminRequest.delete(`${state.route}/${id}`)
|
||||||
|
init()
|
||||||
|
ElMessage.success("删除成功~")
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error("删除失败~")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码改变时的处理函数
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
state.query.page = page
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
// 页面加载时获取交易记录
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
:deep(.el-upload) {
|
||||||
|
border: 1px dashed var(--el-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: var(--el-transition-duration-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload:hover) {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon.avatar-uploader-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.avatar{
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
11
ui/src/pages/admin/transpositional.vue
Normal file
11
ui/src/pages/admin/transpositional.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
208
ui/src/pages/admin/user.vue
Normal file
208
ui/src/pages/admin/user.vue
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form :inline="true" :model="state.query" class="demo-form-inline">
|
||||||
|
<el-form-item label="账号:">
|
||||||
|
<el-input v-model="state.query.username" placeholder="请输入账号" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="init">查询</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item >
|
||||||
|
<el-button type="success" @click="openAddDialog">新增</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card shadow="never">
|
||||||
|
<!--表格-->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-table :data="state.getList">
|
||||||
|
<!-- 列表结开始-->
|
||||||
|
<el-table-column prop="username" label="账号" align="center"/>
|
||||||
|
<el-table-column prop="nickName" label="昵称" align="center"/>
|
||||||
|
<el-table-column prop="phone" label="手机号" align="center"/>
|
||||||
|
<el-table-column label="密钥" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button type="primary" size="small" @click="viewHex(scope.row.hex)">查看密钥</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="createDate" label="创建时间" align="center"/>
|
||||||
|
<!--列表结束-->
|
||||||
|
<el-table-column label="操作" align="center" >
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="edit(scope.row)" size="small">编辑</el-button>
|
||||||
|
<el-button @click="del(scope.row.id)" type="danger" size="small">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template v-slot:empty>
|
||||||
|
<el-empty description="数据去外太空了~" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container" v-if="state.query.total > 0">
|
||||||
|
<el-pagination
|
||||||
|
:current-page="state.query.page"
|
||||||
|
:page-size="state.query.limit"
|
||||||
|
:total="state.query.total"
|
||||||
|
:page-sizes="[5,10,20,50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
background
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog v-model="state.dialogVisible" title="新增" width="50%">
|
||||||
|
<el-form :model="state.formData" ref="formRef" label-width="100px">
|
||||||
|
<!-- 表单开始===============================================================================================================================-->
|
||||||
|
<el-form-item label="账号" prop="username" :rules="[{ required: true, message: '请输入账号', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="state.formData.username"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" >
|
||||||
|
<el-input v-model="state.formData.password"/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="昵称" prop="nickName" :rules="[{ required: true, message: '请输入昵称', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="state.formData.nickName"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" prop="phone" :rules="[{ required: true, message: '请输入手机号', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="state.formData.phone"/>
|
||||||
|
</el-form-item>
|
||||||
|
<!--表单结束===============================================================================================================================-->
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="state.dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveTransaction(formRef)">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessageBox, FormInstance } from 'element-plus'
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const state = reactive({
|
||||||
|
route:"sys/user-front",
|
||||||
|
dialogVisible:false,
|
||||||
|
getList: [],//获取数据
|
||||||
|
query:{
|
||||||
|
total: 0, // 总记录数
|
||||||
|
page: 1, // 当前页码
|
||||||
|
limit: 10, // 每页显示的记录数
|
||||||
|
username: "",
|
||||||
|
},
|
||||||
|
formData:{}
|
||||||
|
})
|
||||||
|
// 查看密钥弹窗
|
||||||
|
const viewHex = (hex: string) => {
|
||||||
|
ElMessageBox.alert(hex, '密钥内容', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
type: 'info'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 生成年份列表 1990-2015
|
||||||
|
const years = [];
|
||||||
|
for (let year = 1990; year <= 2015; year++) {
|
||||||
|
years.push(year.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取交易记录
|
||||||
|
const init = () => {
|
||||||
|
adminRequest.get(`${state.route}/page`, {
|
||||||
|
params: state.query
|
||||||
|
}).then((res:any) => {
|
||||||
|
state.getList = res.data.list
|
||||||
|
state.query.total = res.data.total
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 新增交易记录
|
||||||
|
const openAddDialog = () => {
|
||||||
|
state.formData = {}
|
||||||
|
state.dialogVisible = true
|
||||||
|
}
|
||||||
|
// 编辑交易记录
|
||||||
|
const edit = (row: any) => {
|
||||||
|
state.formData = row
|
||||||
|
delete state.formData.password
|
||||||
|
state.dialogVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存记录
|
||||||
|
const saveTransaction = (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
formEl.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
if (state.formData.id) {
|
||||||
|
// 更新交易记录
|
||||||
|
adminRequest.put(`${state.route}`, state.formData).then(()=>{
|
||||||
|
init()
|
||||||
|
state.dialogVisible = false
|
||||||
|
ElMessage.success("提交成功~")
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 新增交易记录
|
||||||
|
adminRequest.post(`${state.route}`, state.formData).then(()=>{
|
||||||
|
init()
|
||||||
|
state.dialogVisible = false
|
||||||
|
ElMessage.success("提交成功~")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 删除记录
|
||||||
|
const del = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await adminRequest.delete(`${state.route}/${id}`)
|
||||||
|
init()
|
||||||
|
ElMessage.success("删除成功~")
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error("删除失败~")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码改变时的处理函数
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
state.query.page = page
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
// 页面加载时获取交易记录
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex; justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
339
ui/src/pages/chat/index.vue
Normal file
339
ui/src/pages/chat/index.vue
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
<template>
|
||||||
|
<div class="cart-container">
|
||||||
|
<h1 class="cart-title">我的购物车</h1>
|
||||||
|
<div v-if="state.getList.length > 0">
|
||||||
|
<div
|
||||||
|
class="cart-item"
|
||||||
|
v-for="(item, index) in state.getList"
|
||||||
|
:key="item.id"
|
||||||
|
>
|
||||||
|
<img :src="item.item.image" class="item-image" />
|
||||||
|
<div class="item-details">
|
||||||
|
<h3>{{ item.item.title }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="quantity-control">
|
||||||
|
<button
|
||||||
|
class="quantity-btn"
|
||||||
|
@click="updateQuantity(item, -1)"
|
||||||
|
:disabled="item.quantity === 1"
|
||||||
|
>
|
||||||
|
<span class="icon">−</span>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="quantity-input"
|
||||||
|
v-model.number="item.quantity"
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
<button class="quantity-btn" @click="updateQuantity(item, 1)">
|
||||||
|
<span class="icon">+</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="item-price">
|
||||||
|
¥{{ (item.price * item.quantity).toFixed(2) }}
|
||||||
|
</div>
|
||||||
|
<button class="delete-btn" @click="removeItem(item.id)">
|
||||||
|
<span class="trash-icon">🗑️</span>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="total-section">
|
||||||
|
<div class="total-price">
|
||||||
|
总计:¥{{ totalPrice.toFixed(2) }}
|
||||||
|
</div>
|
||||||
|
<!-- 收货地址选择 -->
|
||||||
|
<div class="address-section">
|
||||||
|
<label for="shipping-address">收货地址:</label>
|
||||||
|
<select id="shipping-address" v-model="selectedAddress">
|
||||||
|
<option value="">请选择收货地址</option>
|
||||||
|
<option
|
||||||
|
v-for="addr in state.addressList"
|
||||||
|
:key="addr.id"
|
||||||
|
:value="addr.id"
|
||||||
|
>
|
||||||
|
{{addr.recipientName}}- {{ addr.city }}-{{addr.address}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="checkout-btn"
|
||||||
|
:disabled="!selectedAddress"
|
||||||
|
@click="checkout"
|
||||||
|
>
|
||||||
|
<span class="payment-icon">💳</span>
|
||||||
|
立即结算
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-cart">
|
||||||
|
<span class="cart-icon">🛒</span>
|
||||||
|
购物车是空的,快去选购房源吧!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, computed, onMounted } from "vue";
|
||||||
|
// 购物车数据
|
||||||
|
const state = reactive(<any>{
|
||||||
|
getList: [],
|
||||||
|
addressList:[]
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 计算总价
|
||||||
|
*/
|
||||||
|
const totalPrice = computed(() =>
|
||||||
|
state.getList.reduce((sum:any, item:any) => sum + item.price * item.quantity, 0)
|
||||||
|
);
|
||||||
|
/**
|
||||||
|
* 修改房源数量
|
||||||
|
*/
|
||||||
|
const updateQuantity = (data: any, sum: number) => {
|
||||||
|
data.quantity = Number(data.quantity) + sum;
|
||||||
|
frontRequest.put("/api/cart", data).then(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除购物车房源
|
||||||
|
*/
|
||||||
|
const removeItem = (id: number) => {
|
||||||
|
frontRequest.delete(`/api/cart/${id}`).then(() => {
|
||||||
|
ElMessage.success("删除成功");
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化购物车数据
|
||||||
|
*/
|
||||||
|
async function init() {
|
||||||
|
try {
|
||||||
|
const res = await frontRequest.get("/api/cart/page");
|
||||||
|
const address = await frontRequest.get("/api/address/page",{params:{limit:999}});
|
||||||
|
state.getList = res.data.list;
|
||||||
|
state.addressList = address.data.list;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("请求出错:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(async () => {
|
||||||
|
await init();
|
||||||
|
});
|
||||||
|
// 选中的收货地址,默认未选择
|
||||||
|
const selectedAddress = ref("");
|
||||||
|
|
||||||
|
// 结算处理方法
|
||||||
|
const checkout = () => {
|
||||||
|
if (!selectedAddress.value) {
|
||||||
|
ElMessage.error("请先选择收货地址!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let resList = []
|
||||||
|
for (let i = 0; i < state.getList.length; i++) {
|
||||||
|
resList.push({id:state.getList[i].item.id,quantity:state.getList[i].quantity,cartId:state.getList[i].id})
|
||||||
|
}
|
||||||
|
let data = {
|
||||||
|
dtoList:resList,
|
||||||
|
addressId:selectedAddress.value
|
||||||
|
}
|
||||||
|
frontRequest.post("/api/order/cart",data).then(res =>{
|
||||||
|
init()
|
||||||
|
ElMessage.success('下单成功~')
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 海鲜市场风格:清新海洋蓝绿色调 */
|
||||||
|
.cart-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
background: linear-gradient(135deg, #e0f7fa, #b2ebf2);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 150, 136, 0.2);
|
||||||
|
padding: 2rem;
|
||||||
|
border: 1px solid #4db6ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-title {
|
||||||
|
font-size: 2.8rem;
|
||||||
|
color: #006064;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100px 2fr 1fr 1fr 120px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #b2dfdb;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 150, 136, 0.15);
|
||||||
|
transition: transform 0.3s, box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-item:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 150, 136, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-image {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #4db6ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-details h3 {
|
||||||
|
color: #006064;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-btn {
|
||||||
|
background: #4db6ac;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 0.8rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-btn:disabled {
|
||||||
|
background: #b2dfdb;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-btn:hover:enabled {
|
||||||
|
background: #00897b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-input {
|
||||||
|
background: #e0f2f1;
|
||||||
|
border: 1px solid #4db6ac;
|
||||||
|
color: #006064;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.3rem;
|
||||||
|
text-align: center;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-price {
|
||||||
|
color: #006064;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
background: #ff8a80;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 0.8rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: #ff5252;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-section {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-top: 2rem;
|
||||||
|
border: 1px solid #b2dfdb;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-price {
|
||||||
|
color: #006064;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-section {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-section label {
|
||||||
|
color: #006064;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-section select {
|
||||||
|
background: #e0f2f1;
|
||||||
|
border: 1px solid #4db6ac;
|
||||||
|
color: #006064;
|
||||||
|
padding: 0.6rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-btn {
|
||||||
|
background: #4db6ac;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-btn:disabled {
|
||||||
|
background: #b2dfdb;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-btn:hover:enabled {
|
||||||
|
background: #00897b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-cart {
|
||||||
|
color: #006064;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端响应式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.cart-item {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-image {
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "front"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
90
ui/src/pages/collect/index.vue
Normal file
90
ui/src/pages/collect/index.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<!--用户详情-->
|
||||||
|
<template>
|
||||||
|
<el-tabs v-model="state.activeName" >
|
||||||
|
<el-tab-pane v-for="status in state.getOrderStatus" :label="status.name" :name="status.status">
|
||||||
|
<el-table :data="state.getList">
|
||||||
|
<!-- 列表结开始-->
|
||||||
|
<el-table-column prop="title" label="标题" align="center" />
|
||||||
|
<el-table-column prop="name" label="小区名称" align="center" />
|
||||||
|
|
||||||
|
<el-table-column prop="largePic" label="图片" align="center" >
|
||||||
|
<template #default="{ row }">
|
||||||
|
<!-- 点击图片后显示弹框预览 -->
|
||||||
|
<el-image
|
||||||
|
:src="row.image"
|
||||||
|
fit="cover"
|
||||||
|
:preview-src-list="[row.image]"
|
||||||
|
:preview-teleported="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="price" label="价格" align="center" />
|
||||||
|
<el-table-column prop="area" label="面积" align="center" />
|
||||||
|
<el-table-column prop="type" label="房屋类型" align="center" />
|
||||||
|
<el-table-column prop="city" label="城市" align="center" width="160"/>
|
||||||
|
<!--列表结束-->
|
||||||
|
<el-table-column label="操作" align="center" width="160" >
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="del(scope.row.id)" type="danger" size="small">取消</el-button>
|
||||||
|
<el-button @click="to(scope.row.id)" size="small">去换住</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template v-slot:empty>
|
||||||
|
<el-empty description="数据去外太空了~" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const router = useRouter()
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
const state = reactive({
|
||||||
|
activeName:1,
|
||||||
|
getOrderStatus:[
|
||||||
|
{name:"全部",status:1},
|
||||||
|
],
|
||||||
|
getList:[]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* 跳转
|
||||||
|
*/
|
||||||
|
const to = (id:number) => {
|
||||||
|
router.push(`/info/${id}`)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取列表数据
|
||||||
|
*/
|
||||||
|
function init() {
|
||||||
|
frontRequest.get("/api/item/list").then(res =>{
|
||||||
|
state.getList = res.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const del = (id:number) => {
|
||||||
|
frontRequest.post("/api/behavior/delete",{itemId:id,type:1}).then(res =>{
|
||||||
|
ElMessage.success("收藏成功")
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(()=>{
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "front"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
67
ui/src/pages/index.vue
Normal file
67
ui/src/pages/index.vue
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<!--轮播图-->
|
||||||
|
<el-row :gutter="20">
|
||||||
|
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="h-300px">
|
||||||
|
<carousel></carousel>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<div style="padding-top: 20px"></div>
|
||||||
|
<!-- 推荐房源列表-->
|
||||||
|
<item :get-list=state.getList></item>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Carousel from '~/components/front/carousel.vue'
|
||||||
|
import Item from '~/components/front/item.vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const router = useRouter()
|
||||||
|
const state = reactive({
|
||||||
|
getList: [],
|
||||||
|
commit: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
// frontRequest.get("/api/user/userInfo").then(response =>{
|
||||||
|
// user.frontUserInfo = response.data
|
||||||
|
// })
|
||||||
|
|
||||||
|
onMounted(()=>{
|
||||||
|
frontRequest.get("/api/item/score").then(res =>{
|
||||||
|
state.getList = res.data
|
||||||
|
})
|
||||||
|
|
||||||
|
frontRequest.get("/api/item/commit").then(res =>{
|
||||||
|
state.commit = res.data
|
||||||
|
})
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* 跳转
|
||||||
|
*/
|
||||||
|
const to = (id:number) => {
|
||||||
|
router.push(`/info/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.hot {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2); /* 更浅的黑色 */
|
||||||
|
border-radius: 10px; /* 四周圆弧,值可以根据需要调整 */
|
||||||
|
}
|
||||||
|
:deep(.el-button){
|
||||||
|
color: #2e191e;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
:deep(.el-button:hover) {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "front"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user