init: 初始化
This commit is contained in:
commit
332a3fbeac
|
@ -0,0 +1,22 @@
|
|||
NODE_ENV=development
|
||||
# 指定站点名称
|
||||
# 本地开发状态:localhost
|
||||
# 部署时:大多数情况下不需要填写
|
||||
VITE_SITEHOST=localhost
|
||||
# 启动站点API接口地址
|
||||
VITE_SITEHOST_API=http://47.103.207.8:8132
|
||||
# 路由根地址
|
||||
VITE_ROUTER_BASE=/
|
||||
|
||||
# 应用码
|
||||
VITE_APPCODE=hospital
|
||||
VITE_TOOL_ICONS=//at.alicdn.com/t/c/font_4255235_cqom7h3serb.js
|
||||
VITE_MICROLAYOUT_ICONS=//at.alicdn.com/t/c/font_4255235_cqom7h3serb.js
|
||||
|
||||
|
||||
# 是否开启登录测试
|
||||
VITE_LOGIN_TEST=false
|
||||
# 登录测试账号
|
||||
VITE_LOGIN_TEST_USERNAME=admin
|
||||
# 登录测试账号密码
|
||||
VITE_LOGIN_TEST_PASSWORD="234@#$"
|
|
@ -0,0 +1,5 @@
|
|||
# 路由根地址
|
||||
VITE_ROUTER_BASE=/
|
||||
|
||||
# 应用码
|
||||
VITE_APPCODE=hospital
|
|
@ -0,0 +1,23 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-essential',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
parser: '@typescript-eslint/parser',
|
||||
},
|
||||
plugins: ['vue', '@typescript-eslint', 'prettier'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
node_modules
|
||||
dist
|
||||
source
|
||||
|
||||
pnpm-lock.yaml
|
||||
visualizer.html
|
||||
|
||||
tsconfig.tsbuildinfo
|
||||
yaml-tempfile
|
||||
|
||||
.DS_Store
|
||||
**/.DS_Store
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"semi": true,
|
||||
"tabWidth": 3,
|
||||
"useTabs": false,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"printWidth": 120,
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "lf",
|
||||
"vueIndentScriptAndStyle": false
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
// 控制相关文件嵌套展示
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.expand": false,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"*.ts": "$(capture).test.ts, $(capture).test.tsx",
|
||||
"*.tsx": "$(capture).test.ts, $(capture).test.tsx",
|
||||
"package.json": "index.html,pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitignore,.gitattributes,.gitpod.yml,CNAME,README*,.npmrc,.browserslistrc,.env.*,.eslintrc.js,.prettierrc,env.d.ts",
|
||||
"vite.config.ts": "tsconfig.*.json,postcss.config.ts,tailwind.config.ts,tsconfig.json"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "always"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
# 502404_ManageDb
|
||||
|
||||
### 系统管理平台
|
||||
|
||||
基础后台管理,无组织管理,包含功能:
|
||||
|
||||
#### 基础配置
|
||||
- 1、启动配置
|
||||
- 2、类别管理
|
||||
- 3、行政管理
|
||||
|
||||
#### 客户管理
|
||||
- 4、项目管理
|
||||
- 5、用户管理
|
||||
- 6、用户组管理 TODO
|
||||
- 7、权限管理(基于用户组) TODO
|
||||
|
||||
#### 公共管理
|
||||
- 9、任务管理
|
||||
- 10、任务日志
|
||||
- 11、天气查询
|
||||
- 12、登录日志
|
||||
|
||||
|
||||
### 安装使用步骤
|
||||
|
||||
- **Install:**
|
||||
|
||||
```text
|
||||
pnpm install
|
||||
```
|
||||
|
||||
- **Run:**
|
||||
|
||||
```text
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- **Build:**
|
||||
|
||||
```text
|
||||
pnpm build
|
||||
```
|
||||
|
||||
- **preview:**
|
||||
|
||||
```text
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
### 文件资源目录 📚
|
||||
|
||||
```text
|
||||
502402_BasicManage
|
||||
├─ .vscode # VSCode 推荐配置
|
||||
├─ mock # Mock模拟数据
|
||||
├─ public # 静态资源文件
|
||||
│ ├─ assets # 静态资源文件
|
||||
├─ src
|
||||
│ ├─ api # API 接口管理
|
||||
│ │ └─ typings # 接口 ts 声明
|
||||
│ ├─ assets # 静态资源文件
|
||||
│ │ └─ styles # 全局样式文件
|
||||
│ ├─ const # 全局 常量 声明(颜色/长宽)
|
||||
│ ├─ components # 全局组件
|
||||
│ ├─ router # 路由管理
|
||||
│ ├─ stores # pinia store
|
||||
│ ├─ utils # 常用工具库
|
||||
│ ├─ views # 项目所有页面
|
||||
│ ├─ App.vue # 项目主组件
|
||||
│ ├─ main.ts # 项目入口文件
|
||||
│ └─ vite-env.d.ts # 指定 ts 识别 vue
|
||||
├─ .env # vite 常用配置
|
||||
├─ .env.development # 开发环境配置
|
||||
├─ .env.production # 生产环境配置
|
||||
├─ .env.test # 测试环境配置
|
||||
├─ .prettierrc # vs代码规范文件
|
||||
├─ index.html # 入口 html
|
||||
├─ pnpm-lock.yaml # 依赖包包版本锁
|
||||
├─ package.json # 依赖包管理
|
||||
├─ README.md # README 介绍
|
||||
├─ tsconfig.json # typescript 全局配置
|
||||
└─ vite.config.ts # vite 全局配置文件
|
||||
```
|
||||
|
||||
### 开发环境 🔨
|
||||
|
||||
- 使用 Vue3.3 + TypeScript + Vite4 + Pinia + VueRouter开发
|
||||
- UI组件: ant-design-vue
|
||||
- CSS预处理器: sass
|
||||
- vite插件: axios
|
||||
- Node环境: v18.16.0
|
||||
|
||||
### Git commit ⻛格指南
|
||||
|
||||
- feat: 增加新功能
|
||||
- fix: 修复问题
|
||||
- style: 代码⻛格相关⽆影响运⾏结果的
|
||||
- perf: 优化/性能提升
|
||||
- refactor: 重构
|
||||
- revert: 撤销修改
|
||||
- test: 测试相关
|
||||
- docs: ⽂档/注释
|
||||
- chore: 依赖更新/脚⼿架配置修改等
|
||||
- ci: 持续集成
|
|
@ -0,0 +1,11 @@
|
|||
declare module '*.vue' {
|
||||
import { ComponentOptions } from 'vue';
|
||||
const componentOptions: ComponentOptions;
|
||||
export default componentOptions;
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="null">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>后台管理系统</title>
|
||||
<style>
|
||||
html {
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"name": "basic-manager",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"main": "main.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"gen": "yaml-tempfile",
|
||||
"build": "vue-tsc -b && vite build --mode production",
|
||||
"build:test": "vue-tsc && vite build --mode test",
|
||||
"build:pro": "vue-tsc && vite build --mode production",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"ace-builds": "^1.37.1",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"dayjs": "^1.11.13",
|
||||
"pinia": "^2.3.0",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-draggable-next": "^2.2.1",
|
||||
"vue-m-message": "^4.0.2",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue3-ace-editor": "^2.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@skyfox2000/vite-plugin-auto-import-dts": "^1.0.3",
|
||||
"@types/mockjs": "1.0.7",
|
||||
"@types/node": "^22.10.2",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"esbuild": "^0.19.12",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"mockjs": "^1.1.0",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"rollup": "^4.29.1",
|
||||
"rollup-plugin-gzip": "^4.0.1",
|
||||
"rollup-plugin-visualizer": "^5.13.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.7",
|
||||
"vite-plugin-mock": "3.0.1",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
import { Plugin } from 'vite';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* 首字母大写的工具函数
|
||||
*/
|
||||
function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归扫描目录中的 .vue 文件并生成导出语句
|
||||
*/
|
||||
function scanDir(dir: string, baseDir: string): string[] {
|
||||
const result: string[] = [];
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(dir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
// 递归扫描子目录
|
||||
result.push(...scanDir(filePath, baseDir));
|
||||
} else if (file.endsWith('.vue')) {
|
||||
const relativePath = path.relative(baseDir, filePath);
|
||||
const componentName = generateComponentName(filePath, baseDir);
|
||||
result.push(
|
||||
`export { default as ${componentName} } from './${relativePath.replace(
|
||||
/\\/g,
|
||||
'/',
|
||||
)}';`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件路径生成组件名
|
||||
*/
|
||||
function generateComponentName(filePath: string, baseDir: string): string {
|
||||
const dirName = path.basename(path.dirname(filePath));
|
||||
const fileName = path.basename(filePath, '.vue');
|
||||
|
||||
if (fileName === 'index') {
|
||||
return capitalize(dirName);
|
||||
} else {
|
||||
return capitalize(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件主逻辑
|
||||
*/
|
||||
export default function AutoGenerateVue(options: {
|
||||
dir: string;
|
||||
output: string;
|
||||
}): Plugin {
|
||||
return {
|
||||
name: 'vite-plugin-auto-generate-vue',
|
||||
config() {
|
||||
const { dir, output } = options;
|
||||
|
||||
const componentsDir = path.resolve(process.cwd(), dir);
|
||||
const outputPath = path.resolve(process.cwd(), output);
|
||||
|
||||
if (!fs.existsSync(componentsDir)) {
|
||||
throw new Error(
|
||||
`The specified directory "${componentsDir}" does not exist.`,
|
||||
);
|
||||
}
|
||||
|
||||
const exportStatements = scanDir(componentsDir, componentsDir).join('\n');
|
||||
|
||||
// 写入到目标文件
|
||||
fs.writeFileSync(outputPath, exportStatements, 'utf-8');
|
||||
console.log(`[autoGenerateVue] Global components written to ${output}`);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,5 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
|
@ -0,0 +1,23 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg)
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* 按钮组件
|
||||
* @component
|
||||
* @name Button
|
||||
* @summary 提供一个可点击的图标按钮,支持自动切换图标,发送点击事件等功能。
|
||||
*/
|
||||
import { Button } from 'ant-design-vue';
|
||||
import Tooltip from '../tooltip/index.vue';
|
||||
import LayoutIcon from '../icon/layoutIcon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 提示标题
|
||||
* @props
|
||||
* @name content
|
||||
* @type {string}
|
||||
*/
|
||||
tiptext: String,
|
||||
/**
|
||||
* 提示显示位置
|
||||
* @props
|
||||
* @name placement
|
||||
* @type {string}
|
||||
* @default 'top'
|
||||
*/
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'top',
|
||||
},
|
||||
/**
|
||||
* 内置图标属性设置
|
||||
* @props
|
||||
* @name iconProps
|
||||
* @type {object}
|
||||
* @summary 内置图标复杂属性设置,仅支持前置图标
|
||||
*/
|
||||
iconProps: Object,
|
||||
/**
|
||||
* 默认使用框架图标
|
||||
* 其它图标请自定义
|
||||
* @props
|
||||
* @name icon
|
||||
* @type {string}
|
||||
*/
|
||||
icon: String,
|
||||
// /**
|
||||
// * 点击事件
|
||||
// * @props
|
||||
// * @name clickEvent
|
||||
// * @summary 格式 "空间名#事件名",空间名和事件名用#分隔,事件名用.分隔
|
||||
// * @type {string}
|
||||
// */
|
||||
// clickEvent: {
|
||||
// type: String
|
||||
// },
|
||||
// /**
|
||||
// * 点击传输数据
|
||||
// * @props
|
||||
// * @name data
|
||||
// * @summary 点击事件传输的默认数据
|
||||
// * @type {object|string}
|
||||
// */
|
||||
// data: {
|
||||
// type: [Object, String]
|
||||
// }
|
||||
});
|
||||
|
||||
// 定义组件发出的事件
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击事件
|
||||
* @emits
|
||||
* @name click
|
||||
* @summary 图标按钮点击时触发的事件
|
||||
*/
|
||||
'click',
|
||||
]);
|
||||
|
||||
const onClicked = () => {
|
||||
// if (props.clickEvent) {
|
||||
// const eventNames = props.clickEvent.split("#");
|
||||
// if (eventNames.length === 2) {
|
||||
// const $Bus = inject("$" + eventNames[0]) as any;
|
||||
// $Bus.$emit(eventNames[1], props.data);
|
||||
// }
|
||||
// }
|
||||
if (props.iconProps && props.iconProps.icons) {
|
||||
props.iconProps.iconIndex = (props.iconProps.iconIndex + 1) % props.iconProps.icons.length;
|
||||
}
|
||||
emits('click');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tooltip :title="props.tiptext" :disabled="props.tiptext ? undefined : 'disabled'" :placement="placement">
|
||||
<Button class="px-[10px] py-[4px] flex items-center gap-1" v-bind="$attrs" @click="onClicked">
|
||||
<template #icon>
|
||||
<slot name="icon">
|
||||
<LayoutIcon
|
||||
:icon="props.icon"
|
||||
:class="[$attrs.type === 'primary' ? 'ant-btn-primary' : '', 'cursor-pointer w-[17px] h-[17px] mx-auto']"
|
||||
v-bind="props.iconProps"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
<template #default>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</template>
|
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts" setup>
|
||||
import { SERVER_HOST } from '@skyfox2000/fapi';
|
||||
import { createFromIconfont } from '@skyfox2000/webbase';
|
||||
|
||||
/**
|
||||
* 应用图标加载
|
||||
* @returns 应用图标集
|
||||
*/
|
||||
const AppIcon = () => {
|
||||
return createFromIconfont({
|
||||
iconUrl: `${SERVER_HOST.APP_ICONS}`,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载全部应用图标
|
||||
*/
|
||||
const AppIcons = AppIcon()
|
||||
defineProps({
|
||||
icon: {
|
||||
type: String
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<AppIcons v-if="icon" :icon="icon" :class="['w-6', 'h-6', 'text-2xl', 'align-middle', 'mx-auto']" v-bind="$attrs"/>
|
||||
</template>
|
|
@ -0,0 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { onFullscreenClick, useSettingInfo } from '@skyfox2000/webbase';
|
||||
import LayoutIcon from './layoutIcon.vue';
|
||||
const settingStore = useSettingInfo();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutIcon
|
||||
@click.stop="onFullscreenClick"
|
||||
:icon="settingStore.fullscreen ? 'icon-exitscreen' : 'icon-fullscreen'"
|
||||
class="w-[17px] h-[17px]"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,24 @@
|
|||
<script setup lang="ts">
|
||||
import { Popover } from 'ant-design-vue';
|
||||
import LayoutIcon from './layoutIcon.vue';
|
||||
|
||||
defineProps<{
|
||||
text?: string,
|
||||
maxWidth?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover placement="topRight" v-bind="$attrs">
|
||||
<template #content>
|
||||
<slot>
|
||||
<div class="text-[14px]" :style="{ width: maxWidth ? maxWidth : '250px' }">
|
||||
{{ text }}
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
<span class="ml-2">
|
||||
<LayoutIcon icon="icon-question-circle" class="text-[#888]" v-bind="$attrs"/>
|
||||
</span>
|
||||
</Popover>
|
||||
</template>
|
|
@ -0,0 +1,421 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* 图标组件
|
||||
* @component
|
||||
* @name Icon
|
||||
* @summary 提供一个可点击的图标按钮,支持自动切换图标,发送点击事件等功能。
|
||||
*/
|
||||
|
||||
import { ref, computed, inject, watch } from 'vue';
|
||||
import Tooltip from '../tooltip/index.vue';
|
||||
import { getIconTransform } from '@skyfox2000/webbase';
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 点击后自动切换
|
||||
* @props
|
||||
* @name autoSwitch
|
||||
* @type {boolean}
|
||||
* @default true
|
||||
*/
|
||||
autoSwitch: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 提示标题
|
||||
* @props
|
||||
* @name tiptext
|
||||
* @type {string}
|
||||
*/
|
||||
tiptext: {
|
||||
type: String,
|
||||
},
|
||||
/**
|
||||
* 提示尺寸
|
||||
* @props
|
||||
* @name tipsize
|
||||
* @type {string}
|
||||
*/
|
||||
tipsize: {
|
||||
type: String,
|
||||
},
|
||||
/**
|
||||
* 提示背景色
|
||||
* @props
|
||||
* @name tipcolor
|
||||
* @type {string}
|
||||
*/
|
||||
tipcolor: {
|
||||
type: String,
|
||||
},
|
||||
/**
|
||||
* 提示显示位置
|
||||
* @props
|
||||
* @name placement
|
||||
* @type {string}
|
||||
* @default 'top'
|
||||
*/
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'top',
|
||||
},
|
||||
/**
|
||||
* IconFont图标
|
||||
* @props
|
||||
* @name icon
|
||||
* @type {string}
|
||||
* @summary IconFont图标显示,使用sym-开头的使用svg模式显示
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
},
|
||||
/**
|
||||
* IconFont图标组
|
||||
* @props
|
||||
* @name icons
|
||||
* @type {Array<string>}
|
||||
* @summary IconFont图标组显示,使用sym-开头的使用svg模式显示
|
||||
* @default []
|
||||
*/
|
||||
icons: {
|
||||
type: Array<string>,
|
||||
default: () => [],
|
||||
},
|
||||
/**
|
||||
* 图标索引
|
||||
* @props
|
||||
* @name iconIndex
|
||||
* @type {number}
|
||||
* @default 0
|
||||
*/
|
||||
iconIndex: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
/**
|
||||
* 可点击,鼠标手
|
||||
* @props
|
||||
* @name clickable
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
clickable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 点击事件
|
||||
* @props
|
||||
* @name clickEvent
|
||||
* @summary 格式 "空间名#事件名",空间名和事件名用#分隔,事件名用.分隔
|
||||
* @type {string}
|
||||
*/
|
||||
clickEvent: {
|
||||
type: String,
|
||||
},
|
||||
/**
|
||||
* 点击传输数据
|
||||
* @props
|
||||
* @name data
|
||||
* @summary 点击事件传输的默认数据
|
||||
* @type {object|string}
|
||||
*/
|
||||
data: {
|
||||
type: [Object, String],
|
||||
},
|
||||
/**
|
||||
* 字体大小
|
||||
* @props
|
||||
* @name size
|
||||
* @summary 字体大小
|
||||
* @type {string}
|
||||
* @default 20px
|
||||
*/
|
||||
fontsize: {
|
||||
type: String, // 大小
|
||||
default: '20px',
|
||||
},
|
||||
/**
|
||||
* 空间大小
|
||||
* @props
|
||||
* @name size
|
||||
* @summary 空间大小
|
||||
* @type {string, [string, string]}
|
||||
* @default 20px
|
||||
*/
|
||||
size: {
|
||||
type: [String, Array<String>], // 大小
|
||||
default: () => ['20px', '20px'],
|
||||
},
|
||||
/**
|
||||
* 图标位置
|
||||
* @props
|
||||
* @name position
|
||||
* @summary 图标位置
|
||||
* @type {[string|number, string|number]}
|
||||
*/
|
||||
position: {
|
||||
type: Array<string | number>, // 图标位置
|
||||
},
|
||||
/**
|
||||
* 旋转中心
|
||||
* @props
|
||||
* @name center
|
||||
* @summary 旋转中心
|
||||
* @type {string}
|
||||
*/
|
||||
center: {
|
||||
type: String, // 旋转中心
|
||||
},
|
||||
/**
|
||||
* 指定角度
|
||||
* @props
|
||||
* @name angle
|
||||
* @summary 指定角度
|
||||
* @type {number}
|
||||
*/
|
||||
angle: {
|
||||
type: Number, // 指定角度
|
||||
},
|
||||
/**
|
||||
* 颜色
|
||||
* @props
|
||||
* @name color
|
||||
* @summary 颜色
|
||||
* @type {string}
|
||||
*/
|
||||
color: {
|
||||
type: String, // 颜色
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* 样式
|
||||
* @props
|
||||
* @name hovercolor
|
||||
* @summary 样式
|
||||
* @type {string}
|
||||
*/
|
||||
className: {
|
||||
type: String, // 样式
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* 水平翻转
|
||||
* @props
|
||||
* @name flip
|
||||
* @summary 水平翻转
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
flip: {
|
||||
type: Boolean, // 水平翻转
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 自动旋转
|
||||
* @props
|
||||
* @name spin
|
||||
* @summary 自动旋转
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
spin: {
|
||||
type: Boolean, // 自动旋转
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 定义组件发出的事件
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击事件
|
||||
* @emits
|
||||
* @name click
|
||||
* @summary 图标按钮点击时触发的事件
|
||||
*/
|
||||
'click',
|
||||
/**
|
||||
* 图标索引更新
|
||||
* @emits
|
||||
* @name update:iconIndex
|
||||
* @summary 图标索引发生变化时触发的事件
|
||||
* @param {number} index - 新的图标索引
|
||||
*/
|
||||
'update:iconIndex',
|
||||
]);
|
||||
|
||||
const boxSize = computed(() => {
|
||||
if (Array.isArray(props.size)) return props.size;
|
||||
else {
|
||||
let size = props.size;
|
||||
return [size, size];
|
||||
}
|
||||
});
|
||||
|
||||
const curIcon = ref(props.icon);
|
||||
const curIndex = ref(props.iconIndex);
|
||||
|
||||
watch(
|
||||
() => props.icon,
|
||||
(newVal) => {
|
||||
curIcon.value = newVal;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.iconIndex,
|
||||
(newVal) => {
|
||||
curIndex.value = newVal;
|
||||
curIcon.value = props.icons[newVal];
|
||||
},
|
||||
);
|
||||
|
||||
const iconIndex = computed<number>({
|
||||
get() {
|
||||
return curIndex.value;
|
||||
},
|
||||
set(val) {
|
||||
curIndex.value = val;
|
||||
emits('update:iconIndex', curIndex.value);
|
||||
},
|
||||
});
|
||||
|
||||
if (props.icons.length > 0) {
|
||||
iconIndex.value = iconIndex.value >= props.icons.length ? 0 : iconIndex.value;
|
||||
curIcon.value = props.icons[curIndex.value];
|
||||
} else {
|
||||
curIcon.value = props.icon;
|
||||
}
|
||||
|
||||
const onClicked = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (props.clickEvent) {
|
||||
const eventNames = props.clickEvent.split('#');
|
||||
if (eventNames.length === 2) {
|
||||
const $Bus = inject('$' + eventNames[0]) as any;
|
||||
$Bus.$emit(eventNames[1], props.data);
|
||||
}
|
||||
}
|
||||
if (props.autoSwitch && props.icons.length > 0) {
|
||||
iconIndex.value = (iconIndex.value + 1) % props.icons.length;
|
||||
curIcon.value = props.icons[curIndex.value];
|
||||
}
|
||||
|
||||
emits('click');
|
||||
};
|
||||
|
||||
const getIconClass = (): string => {
|
||||
let className = '';
|
||||
if (props.spin) {
|
||||
className += 'rotate';
|
||||
}
|
||||
if (props.flip) {
|
||||
className += ' flip';
|
||||
}
|
||||
return className;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Tooltip
|
||||
:title="props.tiptext"
|
||||
:disabled="props.tiptext ? undefined : 'disabled'"
|
||||
:color="tipcolor"
|
||||
:placement="placement"
|
||||
:size="tipsize"
|
||||
>
|
||||
<div
|
||||
class="re-icon-container"
|
||||
v-if="curIcon"
|
||||
:style="{
|
||||
width: boxSize[0].toString(),
|
||||
height: boxSize[1].toString(),
|
||||
}"
|
||||
>
|
||||
<!--
|
||||
使用动态 class 绑定,通过 icon-class 控制图标样式,curIcon 是计算属性,
|
||||
可以根据 iconIndex 控制图标显示,点击图标时执行 onClicked 方法
|
||||
-->
|
||||
<i
|
||||
v-bind="$attrs"
|
||||
v-if="!curIcon?.startsWith('sym-')"
|
||||
class="re-icon iconfont fontclass"
|
||||
:class="[props.clickable ? 'clickable' : '', 'icon-' + curIcon, getIconClass(), props.className]"
|
||||
:style="{
|
||||
top: props.position ? props.position[1] : 1,
|
||||
left: props.position ? props.position[0] : 0,
|
||||
fontSize: props.fontsize,
|
||||
transformOrigin: props.center ?? 'center center',
|
||||
transform: getIconTransform(props.angle, props.flip),
|
||||
color: props.color,
|
||||
}"
|
||||
aria-hidden="true"
|
||||
@click="onClicked"
|
||||
></i>
|
||||
<svg
|
||||
v-else
|
||||
v-bind="$attrs"
|
||||
class="re-icon symbol"
|
||||
aria-hidden="true"
|
||||
:class="[props.clickable ? 'clickable' : '', getIconClass(), props.className]"
|
||||
:style="{
|
||||
top: props.position ? props.position[1] : 0,
|
||||
left: props.position ? props.position[0] : 0,
|
||||
fontSize: props.fontsize,
|
||||
transformOrigin: props.center ?? 'center center',
|
||||
transform: getIconTransform(),
|
||||
color: props.color,
|
||||
}"
|
||||
>
|
||||
<use :xlink:href="'#icon-' + curIcon.replace('sym-', '')"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.re-icon-container {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
// margin-right: 3px;
|
||||
}
|
||||
|
||||
.re-icon.symbol {
|
||||
position: relative;
|
||||
margin: 5px 0;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.re-icon.fontclass {
|
||||
position: relative;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-stroke-width: 0.2px;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.re-icon[disabled] {
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rotate {
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
.flip {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts" setup>
|
||||
import { SERVER_HOST } from '@skyfox2000/fapi';
|
||||
import { createFromIconfont } from '@skyfox2000/webbase';
|
||||
|
||||
/**
|
||||
* 基座图标加载
|
||||
* @returns 基座图标集
|
||||
*/
|
||||
const LayoutIcon = () => {
|
||||
return createFromIconfont({
|
||||
iconUrl: `${SERVER_HOST.MICROLAYOUT_ICONS}`,
|
||||
monoColor: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 基座图标集
|
||||
*/
|
||||
const LayoutIcons = LayoutIcon();
|
||||
defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
},
|
||||
icons: {
|
||||
type: Array<string>,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<LayoutIcons v-if="icon || icons" :icon="icon" :icons="icons" v-bind="$attrs" />
|
||||
</template>
|
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts" setup>
|
||||
import { SERVER_HOST } from '@skyfox2000/fapi';
|
||||
import { createFromIconfont } from '@skyfox2000/webbase';
|
||||
|
||||
/**
|
||||
* 表格工具图标加载
|
||||
* @returns 表格工具图标加载
|
||||
*/
|
||||
const ToolIcon = () => {
|
||||
return createFromIconfont({
|
||||
iconUrl: `${SERVER_HOST.TOOL_ICONS}`,
|
||||
monoColor: true
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 表格工具图标集
|
||||
*/
|
||||
const ToolIcons = ToolIcon();
|
||||
defineProps({
|
||||
icon: {
|
||||
type: String
|
||||
},
|
||||
icons: {
|
||||
type: Array<string>,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ToolIcons v-if="icon || icons" :icon="icon" :icons="icons" v-bind="$attrs" />
|
||||
</template>
|
|
@ -0,0 +1,12 @@
|
|||
import Tooltip from './tooltip/index.vue';
|
||||
export { Tooltip };
|
||||
import Icon from './icon/index.vue';
|
||||
export { Icon };
|
||||
import Fullscreen from './icon/fullscreen.vue';
|
||||
export { Fullscreen };
|
||||
import Helper from './icon/helper.vue';
|
||||
export { Helper };
|
||||
import ToolIcon from './icon/toolIcon.vue';
|
||||
export { ToolIcon };
|
||||
import Button from './button/index.vue';
|
||||
export { Button };
|
|
@ -0,0 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
import { CSSProperties } from 'vue';
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
const props = defineProps<{
|
||||
size?: string;
|
||||
}>();
|
||||
|
||||
const overlayStyle: CSSProperties = {
|
||||
}
|
||||
|
||||
const overlayInnerStyle: CSSProperties = {
|
||||
}
|
||||
|
||||
if (props.size === 'small') {
|
||||
overlayInnerStyle.fontSize = '12px';
|
||||
overlayInnerStyle.padding = '4px 6px';
|
||||
overlayInnerStyle.minHeight = '26px';
|
||||
overlayStyle.height = '30px';
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Tooltip :overlayInnerStyle="overlayInnerStyle" :overlayStyle="overlayStyle">
|
||||
<slot />
|
||||
</Tooltip>
|
||||
</template>
|
|
@ -0,0 +1,119 @@
|
|||
<script setup lang="ts">
|
||||
import { watch, ref, onMounted } from 'vue';
|
||||
import { Button } from '../../common';
|
||||
import { Modal, Space } from 'ant-design-vue';
|
||||
import { PageData, onFormSave, onFormSaveAs, onFormClose } from '@skyfox2000/webbase';
|
||||
import { AnyData } from '@skyfox2000/fapi';
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* 确认按钮文字,空字符串则不显示
|
||||
*/
|
||||
saveText?: string;
|
||||
/**
|
||||
* 另存为按钮文字,空字符串则不显示
|
||||
*/
|
||||
saveAsText?: string;
|
||||
/**
|
||||
* 取消按钮文字,空字符则不显示
|
||||
*/
|
||||
cancelText?: string;
|
||||
/**
|
||||
* 保存数据请求配置
|
||||
*/
|
||||
pageData?: PageData<AnyData>;
|
||||
/**
|
||||
* 全屏模式
|
||||
*/
|
||||
full?: boolean;
|
||||
}>();
|
||||
|
||||
const editorData = props.pageData?.editor!;
|
||||
const open = ref<boolean>(false);
|
||||
|
||||
watch(
|
||||
() => editorData.visible,
|
||||
(newVal) => {
|
||||
open.value = newVal;
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
open.value = editorData.visible;
|
||||
});
|
||||
|
||||
const dialogSave = () => {
|
||||
if (props.pageData) onFormSave(props.pageData);
|
||||
};
|
||||
const dialogSaveAs = () => {
|
||||
if (props.pageData) onFormSaveAs(props.pageData);
|
||||
};
|
||||
const dialogClose = () => {
|
||||
if (props.pageData) onFormClose(props.pageData);
|
||||
else open.value = false;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Modal
|
||||
v-model:open="open"
|
||||
:wrapClassName="
|
||||
'modal mx-auto ' +
|
||||
($attrs.width ? 'w-[' + $attrs.width + ']' : 'w-[430px]') +
|
||||
' ' +
|
||||
(full ? 'full-modal w-full' : '')
|
||||
"
|
||||
@close="dialogClose"
|
||||
>
|
||||
<slot></slot>
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="dialogClose" v-if="cancelText !== ''">
|
||||
{{ cancelText ?? '取消' }}
|
||||
</Button>
|
||||
<Button
|
||||
@click="dialogSaveAs"
|
||||
v-if="saveAsText !== '' && editorData.saveAsBtnVisible !== false"
|
||||
type="primary"
|
||||
:loading="editorData.isFormSaving"
|
||||
>
|
||||
{{ saveAsText ?? '另存为' }}
|
||||
</Button>
|
||||
<Button
|
||||
@click="dialogSave"
|
||||
v-if="saveText !== '' && editorData.saveBtnVisible !== false"
|
||||
type="primary"
|
||||
:loading="editorData.isFormSaving"
|
||||
>
|
||||
{{ saveText ?? '保存' }}
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
<style>
|
||||
.modal {
|
||||
.ant-modal-content {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.full-modal {
|
||||
.ant-modal {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
top: 0;
|
||||
padding-bottom: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh);
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,144 @@
|
|||
<script setup lang="ts">
|
||||
import { watch, ref, onMounted } from 'vue';
|
||||
import { Button } from '../../common';
|
||||
import { Modal, Space } from 'ant-design-vue';
|
||||
import { AsyncUploader, EditorData, PageData } from '@skyfox2000/webbase';
|
||||
import { AnyData, IUrlInfo } from '@skyfox2000/fapi';
|
||||
import UploadFileList from './uploadList.vue';
|
||||
import { UploadFile } from '@skyfox2000/webbase';
|
||||
import message from 'vue-m-message';
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* 文件后缀限制
|
||||
*/
|
||||
fileExt?: string[];
|
||||
/**
|
||||
* 保存数据请求配置
|
||||
*/
|
||||
pageData: PageData<AnyData>;
|
||||
/**
|
||||
* 最大文件数
|
||||
* 默认1个
|
||||
*/
|
||||
maxCount?: number;
|
||||
/**
|
||||
* 并发上传文件数
|
||||
* 默认3个
|
||||
*/
|
||||
maxConcurrent?: number;
|
||||
/**
|
||||
* 控制弹窗
|
||||
*/
|
||||
uploadForm: EditorData<any>;
|
||||
/**
|
||||
* 上传地址和参数
|
||||
*/
|
||||
url?: IUrlInfo;
|
||||
}>();
|
||||
|
||||
const uploaderForm = ref(props.uploadForm);
|
||||
const open = ref<boolean>(false);
|
||||
|
||||
const maxCount = props.maxCount ?? 1;
|
||||
const maxConcurrent = props.maxConcurrent ?? 3;
|
||||
|
||||
const fileList = ref<UploadFile[]>([]);
|
||||
|
||||
watch(
|
||||
() => uploaderForm.value.visible,
|
||||
() => {
|
||||
open.value = uploaderForm.value.visible;
|
||||
},
|
||||
);
|
||||
|
||||
const dialogUpload = async () => {
|
||||
const url = props.url ?? props.pageData?.urls.upload;
|
||||
if (!url) {
|
||||
message.error('未配置文件上传地址!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!url.api) url.api = props.pageData.api;
|
||||
if (url.authorize === undefined) url.authorize = props.pageData.authorize;
|
||||
|
||||
const uploader = new AsyncUploader(url, maxConcurrent);
|
||||
|
||||
uploaderForm.value.isFormLoading = true;
|
||||
try {
|
||||
if (fileList.value.length === 0) {
|
||||
message.warning('请选择上传的文件!');
|
||||
setTimeout(() => {
|
||||
uploaderForm.value.isFormLoading = false;
|
||||
}, 10000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 开始上传文件
|
||||
await uploader.upload(
|
||||
fileList.value,
|
||||
(file) => {
|
||||
console.log(`${file.name} 上传进度:${file.percent}% (${file.status})`);
|
||||
},
|
||||
(files) => {
|
||||
uploaderForm.value.isFormLoading = false;
|
||||
console.log('所有文件上传结束:', files);
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
uploaderForm.value.isFormLoading = false;
|
||||
console.error('上传错误:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
open.value = uploaderForm.value.visible;
|
||||
});
|
||||
|
||||
const dialogClose = () => {
|
||||
uploaderForm.value.visible = false;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Modal
|
||||
title="文件上传"
|
||||
v-model:open="open"
|
||||
:wrapClassName="'modal mx-auto ' + ($attrs.width ? 'w-[' + $attrs.width + ']' : 'w-[430px]')"
|
||||
@close="dialogClose"
|
||||
>
|
||||
<UploadFileList v-model:file-list="fileList" :max-count="maxCount" :file-ext="fileExt" />
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="dialogClose">取消</Button>
|
||||
<Button @click="dialogUpload" type="primary" :loading="uploaderForm.isFormSaving"> 上传文件 </Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
<style>
|
||||
.modal {
|
||||
.ant-modal-content {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.full-modal {
|
||||
.ant-modal {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
top: 0;
|
||||
padding-bottom: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh);
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,150 @@
|
|||
<script setup lang="ts">
|
||||
import { Button } from '@/components';
|
||||
import { computed, ref } from 'vue';
|
||||
import message from 'vue-m-message';
|
||||
import type { UploadProps } from 'ant-design-vue';
|
||||
import { Upload, Progress, Tag } from 'ant-design-vue';
|
||||
import { UploadFile, UploadFileStatus } from '@skyfox2000/webbase';
|
||||
import { watch } from 'vue';
|
||||
|
||||
interface Props {
|
||||
fileList: UploadFile<any>[];
|
||||
placeholder?: string;
|
||||
fileExt?: string[];
|
||||
maxFileSize?: number;
|
||||
maxCount?: number;
|
||||
autoUpload?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
fileList: () => [],
|
||||
placeholder: '',
|
||||
maxFileSize: 20,
|
||||
maxCount: 5,
|
||||
autoUpload: false,
|
||||
});
|
||||
|
||||
const fileList = ref<UploadFile[]>([]);
|
||||
const fileUploader = ref();
|
||||
const emit = defineEmits(['update:file-list']);
|
||||
|
||||
const acceptString = computed(() => (props.fileExt?.length ? props.fileExt.map((ext) => `.${ext}`).join(',') : ''));
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
if (props.fileExt && props.fileExt.length > 0) {
|
||||
const extension = file.name.split('.').pop()?.toLowerCase() || '';
|
||||
if (!props.fileExt.includes(extension)) {
|
||||
message.error('文件类型不支持');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (file.size / 1024 / 1024 > props.maxFileSize) {
|
||||
message.error(`文件大小超过 ${props.maxFileSize}MB 限制`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fileList.value.length >= props.maxCount) {
|
||||
message.error(`最多上传 ${props.maxCount} 个文件`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return props.autoUpload;
|
||||
};
|
||||
|
||||
const uploadProps = computed<UploadProps>(() => ({
|
||||
accept: acceptString.value,
|
||||
multiple: true,
|
||||
fileList: fileList.value as UploadProps['fileList'],
|
||||
beforeUpload: beforeUpload,
|
||||
listType: 'text',
|
||||
maxCount: props.maxCount,
|
||||
showUploadList: false,
|
||||
onChange: (info) => {
|
||||
if (!props.autoUpload) {
|
||||
fileList.value = info.fileList as unknown as UploadFile[];
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
watch(
|
||||
() => fileList.value,
|
||||
(newVal) => {
|
||||
emit('update:file-list', newVal);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const getPlaceholder = (): string => {
|
||||
const typeMsg = props.fileExt && props.fileExt.length ? `文件必须为 ${props.fileExt.join('/')}` : '';
|
||||
const sizeMsg = props.maxFileSize !== 0 ? `单文件最大 ${props.maxFileSize}MB` : '';
|
||||
const countMsg = props.maxCount !== 0 ? `最多 ${props.maxCount} 个文件` : '';
|
||||
|
||||
return [sizeMsg, typeMsg, countMsg].filter(Boolean).join(',');
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
fileList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const getStatusColor = (status?: UploadFileStatus) => {
|
||||
switch (status) {
|
||||
case UploadFileStatus.Uploading:
|
||||
return 'blue';
|
||||
case UploadFileStatus.Success:
|
||||
return 'green';
|
||||
case UploadFileStatus.Error:
|
||||
return 'red';
|
||||
default:
|
||||
return 'cyan';
|
||||
}
|
||||
};
|
||||
const getStatus = (status?: UploadFileStatus) => {
|
||||
switch (status) {
|
||||
case UploadFileStatus.Uploading:
|
||||
return '上传中';
|
||||
case UploadFileStatus.Success:
|
||||
return '已完成';
|
||||
case UploadFileStatus.Error:
|
||||
return '上传失败';
|
||||
default:
|
||||
return '待上传';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full border border-solid border-gray-100 mt-1 rounded-md py-5">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="w-35 mx-3">
|
||||
<Upload ref="fileUploader" v-bind="uploadProps">
|
||||
<Button>选择文件</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
<div class="flex-1 text-sm text-gray-500">{{ getPlaceholder() }}</div>
|
||||
<!-- <Button v-if="!autoUpload" @click="manualUpload" class="mr-3">开始上传</Button> -->
|
||||
</div>
|
||||
|
||||
<div class="mt-4 px-3">
|
||||
<div v-for="(file, index) in fileList" :key="index" class="mb-2 pb-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-700 mr-2">{{ file.name }}</span>
|
||||
<span>
|
||||
<Tag :color="getStatusColor(file.status)">{{ getStatus(file.status) }}</Tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<!-- <span class="text-blue-500 hover:text-blue-700 mr-4">预览</span> -->
|
||||
<span class="text-red-500 hover:text-red-700 cursor-pointer" @click="removeFile(index)">删除</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传进度条 -->
|
||||
<div>
|
||||
<Progress :percent="file.percent" :stroke-width="2" :show-info="false" style="height: 2px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,110 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { Button } from '../../common';
|
||||
import { Drawer, Space, theme } from 'ant-design-vue';
|
||||
const { useToken } = theme;
|
||||
const { token } = useToken();
|
||||
import { PageData, onFormSave, onFormSaveAs, onFormClose } from '@skyfox2000/webbase';
|
||||
import { AnyData } from '@skyfox2000/fapi';
|
||||
import LayoutIcon from '@/components/common/icon/layoutIcon.vue';
|
||||
|
||||
const open = ref<boolean>(false);
|
||||
|
||||
// interface RuleItem {
|
||||
// key: string[]; // 使用字符串数组来表示嵌套属性的路径
|
||||
// validate: (value: any) => boolean; //
|
||||
// message: string
|
||||
// }
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* 确认按钮文字,空字符串则不显示
|
||||
*/
|
||||
saveText?: string;
|
||||
/**
|
||||
* 另存为按钮文字,空字符串则不显示
|
||||
*/
|
||||
saveAsText?: string;
|
||||
/**
|
||||
* 取消按钮文字,空字符则不显示
|
||||
*/
|
||||
cancelText?: string;
|
||||
/**
|
||||
* 保存数据请求配置
|
||||
*/
|
||||
pageData: PageData<AnyData>;
|
||||
}>();
|
||||
|
||||
const editorData = props.pageData.editor!;
|
||||
watch(
|
||||
() => editorData.visible,
|
||||
() => {
|
||||
open.value = editorData.visible;
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
open.value = props.pageData.editor!.visible;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Drawer
|
||||
v-model:open="open"
|
||||
:get-container="false"
|
||||
:closable="false"
|
||||
:header-style="{
|
||||
height: '40px',
|
||||
padding: '10px 6px 10px 16px',
|
||||
backgroundColor: token.colorBgLayout,
|
||||
}"
|
||||
:body-style="{
|
||||
padding: '10px 16px',
|
||||
}"
|
||||
:footer-style="{
|
||||
textAlign: 'right',
|
||||
}"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
boxShadow: 'rgba(0, 0, 0, 0.3) -2px 0px 8px',
|
||||
}"
|
||||
width="412px"
|
||||
@close="() => onFormClose(pageData)"
|
||||
>
|
||||
<template #extra>
|
||||
<div class="hover:bg-gray-200 w-[24px] h-[24px] rounded-md">
|
||||
<LayoutIcon
|
||||
class="top-[-2px] left-[2px]"
|
||||
icon="add"
|
||||
:angle="45"
|
||||
fontsize="24px"
|
||||
clickable
|
||||
color="#666"
|
||||
:position="[0, 0]"
|
||||
@click="() => onFormClose(pageData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<slot></slot>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="() => onFormClose(pageData)" v-if="cancelText !== ''">{{ cancelText ?? '取消' }}</Button>
|
||||
<Button
|
||||
@click="() => onFormSaveAs(pageData)"
|
||||
v-if="saveAsText !== '' && editorData.saveAsBtnVisible !== false"
|
||||
type="primary"
|
||||
:loading="editorData.isFormSaving"
|
||||
>{{ saveAsText ?? '另存为' }}</Button
|
||||
>
|
||||
<Button
|
||||
@click="() => onFormSave(pageData)"
|
||||
v-if="saveText !== '' && editorData.saveBtnVisible !== false"
|
||||
type="primary"
|
||||
:loading="editorData.isFormSaving"
|
||||
>{{ saveText ?? '保存' }}</Button
|
||||
>
|
||||
</Space>
|
||||
</template>
|
||||
</Drawer>
|
||||
</template>
|
|
@ -0,0 +1,54 @@
|
|||
<script setup lang="ts">
|
||||
import { useFormItemFactory } from '@skyfox2000/webbase';
|
||||
import { FormItem } from 'ant-design-vue';
|
||||
import { Helper } from '@/components/common';
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* 标签文字
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
rule?: string;
|
||||
/**
|
||||
* 帮助文字
|
||||
*/
|
||||
helper?: string;
|
||||
}>();
|
||||
|
||||
const { msg } = useFormItemFactory({
|
||||
label: props.label,
|
||||
rule: props.rule,
|
||||
});
|
||||
|
||||
// 参考代码
|
||||
// this.$parent.$el.scrollIntoView({
|
||||
// //滚动到指定节点
|
||||
// block: "center", //值有start,center,end,nearest,当前显示在视图区域中间
|
||||
// behavior: "smooth" //值有auto、instant,smooth,缓动动画(当前是慢速的)
|
||||
// });
|
||||
</script>
|
||||
<template>
|
||||
<div class="w-full relative mb-1">
|
||||
<FormItem :required="rule !== undefined" class="!w-[95%] relative" v-bind="$attrs" :class="[rule ? '' : 'mb-3']">
|
||||
<template #label>
|
||||
<span :class="[msg ? 'text-[#ff4d4f]' : '', 'w-full']"> {{ label }}</span>
|
||||
</template>
|
||||
<div class="w-full flex items-center">
|
||||
<div class="flex-grow">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="w-8 mt-[-2px]">
|
||||
<slot name="helper">
|
||||
<Helper v-if="helper" :text="helper" />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</FormItem>
|
||||
<span class="absolute bottom-[3px] left-[80px] text-[12px] text-[#ff4d4fcc] block" v-if="msg">
|
||||
{{ msg }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,43 @@
|
|||
<script lang="ts" setup>
|
||||
import { EditorData, PageData, ProviderKeys } from '@skyfox2000/webbase';
|
||||
import { AnyData } from '@skyfox2000/fapi';
|
||||
import { Form } from 'ant-design-vue';
|
||||
import { inject, provide, ref } from 'vue';
|
||||
// 使用以下统一控制表单显示
|
||||
// :label-col="{ flex: '60px' }"
|
||||
// :wrapper-col="{ flex: '200px' }"
|
||||
// 单独项使用FormItem控制
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* 快速配置表单标签宽度
|
||||
*/
|
||||
labelWidth?: string;
|
||||
/**
|
||||
* 快速配置表单项宽度
|
||||
*/
|
||||
wrapperWidth?: string;
|
||||
/**
|
||||
* 表单配置信息
|
||||
*/
|
||||
editorData?: EditorData<AnyData>;
|
||||
}>();
|
||||
|
||||
const pageData = ref<PageData<AnyData> | undefined>(inject(ProviderKeys.PageData, undefined));
|
||||
|
||||
const editorData = props.editorData ?? (pageData.value ? pageData.value.editor : undefined);
|
||||
provide(ProviderKeys.EditorData, editorData);
|
||||
</script>
|
||||
<template>
|
||||
<Form
|
||||
:label-col="{ flex: props.labelWidth ?? '85px' }"
|
||||
:wrapper-col="{
|
||||
flex: props.wrapperWidth ?? '1',
|
||||
}"
|
||||
:style="{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
}"
|
||||
>
|
||||
<slot></slot>
|
||||
</Form>
|
||||
</template>
|
|
@ -0,0 +1,117 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch, useSlots, VNode } from 'vue';
|
||||
import { Form, Space } from 'ant-design-vue';
|
||||
import SearchItem from './searchItem.vue';
|
||||
import { Button } from '../../common';
|
||||
import { PageData } from '@skyfox2000/webbase';
|
||||
import { AnyData } from '@skyfox2000/fapi';
|
||||
// 使用以下统一控制表单显示
|
||||
// :label-col="{ flex: '60px' }"
|
||||
// :wrapper-col="{ flex: '200px' }"
|
||||
// 单独项使用FormItem控制
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* 搜索条件
|
||||
*/
|
||||
search: Record<string, any>;
|
||||
/**
|
||||
* 页面数据
|
||||
*/
|
||||
pageData: PageData<AnyData>;
|
||||
}>();
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:search', val: Record<string, any>): void;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* 搜索按钮前的占位数量,1或者2
|
||||
*/
|
||||
const holderSize = ref(0);
|
||||
const defaultSlots = ref(0);
|
||||
const controlSlots = ref(0);
|
||||
const getSlotLen = (items: VNode[]): number => {
|
||||
let count = 0;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (typeof items[i].type === 'object') count++;
|
||||
}
|
||||
return count;
|
||||
};
|
||||
const slots = useSlots();
|
||||
const updateHolderSize = () => {
|
||||
defaultSlots.value = 0;
|
||||
controlSlots.value = 0;
|
||||
if (slots.default) defaultSlots.value = getSlotLen(slots.default({}));
|
||||
if (props.pageData.searchBar && slots.control) controlSlots.value = getSlotLen(slots.control({}));
|
||||
|
||||
holderSize.value = 2 - ((defaultSlots.value + controlSlots.value) % 3);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.pageData.searchBar,
|
||||
() => {
|
||||
updateHolderSize();
|
||||
},
|
||||
);
|
||||
|
||||
const defaultData: Record<string, any> = JSON.parse(JSON.stringify(props.search));
|
||||
onMounted(() => {
|
||||
updateHolderSize();
|
||||
let search = updateSearchNull();
|
||||
props.pageData.searchQuery = {
|
||||
Query: { ...search },
|
||||
};
|
||||
});
|
||||
|
||||
const onSearch = () => {
|
||||
let search = updateSearchNull();
|
||||
props.pageData.searchQuery = {
|
||||
Query: { ...search },
|
||||
};
|
||||
if (props.pageData.grid) props.pageData.grid.reload = true;
|
||||
};
|
||||
|
||||
const updateSearchNull = (): Record<string, any> => {
|
||||
let search = { ...props.search };
|
||||
for (const key in search) {
|
||||
if (search[key] === null) {
|
||||
search[key] = undefined;
|
||||
}
|
||||
}
|
||||
return search;
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
const data = JSON.parse(JSON.stringify(defaultData));
|
||||
emits('update:search', data);
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Form
|
||||
v-if="defaultSlots + controlSlots > 0"
|
||||
:label-col="{ flex: '60px' }"
|
||||
ref="searchForm"
|
||||
:style="{
|
||||
flexWrap: 'wrap',
|
||||
borderBottom: '1px solid #e9e9e9',
|
||||
}"
|
||||
class="flex mb-[10px]"
|
||||
>
|
||||
<!-- 默认插槽 -->
|
||||
<slot></slot>
|
||||
<!-- 受控插槽 -->
|
||||
<slot name="control" v-if="pageData.searchBar"></slot>
|
||||
<!-- 表单操作按钮 占位数量 -->
|
||||
<SearchItem class="w-[33%]" v-if="holderSize >= 1"> </SearchItem>
|
||||
<SearchItem class="w-[33%]" v-if="holderSize >= 2"> </SearchItem>
|
||||
<SearchItem
|
||||
v-if="defaultSlots || pageData.searchBar"
|
||||
class="w-[33%] flex justify-end text-right pr-2"
|
||||
:wrapper-col="{ flex: 'auto' }"
|
||||
>
|
||||
<Space>
|
||||
<Button type="primary" @click="onSearch">搜索</Button>
|
||||
<Button @click="onReset">重置</Button>
|
||||
</Space>
|
||||
</SearchItem>
|
||||
</Form>
|
||||
</template>
|
|
@ -0,0 +1,43 @@
|
|||
<script setup lang="ts">
|
||||
import { useFormItemFactory } from '@skyfox2000/webbase';
|
||||
import { FormItem } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* 标签文字
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
rule?: string;
|
||||
}>();
|
||||
|
||||
const { msg } = useFormItemFactory({
|
||||
label: props.label,
|
||||
rule: props.rule,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="w-1/3 relative mb-1">
|
||||
<FormItem
|
||||
:required="rule !== undefined"
|
||||
class="!w-[90%] relative"
|
||||
v-bind="$attrs"
|
||||
:class="[rule ? '' : 'mb-3']"
|
||||
:labelCol="{ span: 6 }"
|
||||
>
|
||||
<template #label v-if="label">
|
||||
<span :class="[msg ? 'text-[#ff4d4f]' : '', 'w-full']"> {{ label }}</span>
|
||||
</template>
|
||||
<div class="w-full flex items-center">
|
||||
<div class="flex-grow">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</FormItem>
|
||||
<span class="absolute bottom-[3px] left-[80px] text-[12px] text-[#ff4d4fcc] block" v-if="msg">
|
||||
{{ msg }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,153 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { Table, TablePaginationConfig, TableProps } from 'ant-design-vue';
|
||||
import {
|
||||
gridQueryFind,
|
||||
gridStatusUpdate,
|
||||
PageData,
|
||||
PrimaryKey,
|
||||
useSettingInfo,
|
||||
OPTIONS,
|
||||
gridQueryList,
|
||||
} from '@skyfox2000/webbase';
|
||||
import TableOperate from './tableOperate.vue';
|
||||
import Toolbar from '../toolbar/index.vue';
|
||||
import Switch from '../../form/switch/index.vue';
|
||||
import { RowOperate } from '@skyfox2000/microbase';
|
||||
import { AnyData } from '@skyfox2000/fapi';
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* 页面主数据
|
||||
*/
|
||||
pageData: PageData<AnyData>;
|
||||
}>();
|
||||
|
||||
const pageData = props.pageData;
|
||||
const gridData = pageData.grid!;
|
||||
|
||||
gridData.pageNo = 1;
|
||||
gridData.total = 0;
|
||||
gridData.pageSize = gridData.pageSize ?? 10;
|
||||
|
||||
const dataList = ref<Record<string, AnyData>[]>([]);
|
||||
const pagination = ref<TablePaginationConfig>({
|
||||
total: 0,
|
||||
current: 1,
|
||||
pageSize: gridData.pageSize,
|
||||
showTotal: (total: number) => {
|
||||
return `共 ${total} 条记录`;
|
||||
},
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
pagination.value.current = page;
|
||||
pagination.value.pageSize = pageSize;
|
||||
gridData.pageSize = pageSize;
|
||||
gridData.pageNo = page;
|
||||
gridQueryFind(pageData);
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => gridData.tableData,
|
||||
() => {
|
||||
dataList.value = gridData.tableData!;
|
||||
pagination.value.total = gridData.total ?? 0;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => gridData.reload,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
gridQueryFind(pageData);
|
||||
gridData.reload = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
const columns = ref<Record<string, any>[]>();
|
||||
|
||||
const selection = {
|
||||
onChange: (selectedRowKeys: PrimaryKey[], selectedRows: any[]) => {
|
||||
gridData.selectKeys = selectedRowKeys;
|
||||
gridData.selectRows = selectedRows;
|
||||
},
|
||||
columnWidth: '30px',
|
||||
};
|
||||
const rowSelection = ref<TableProps['rowSelection']>(selection);
|
||||
watch(
|
||||
() => gridData.selectable,
|
||||
(newVal) => {
|
||||
rowSelection.value = newVal ? selection : undefined;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => gridData.columns,
|
||||
() => {
|
||||
columns.value = gridData.columns.filter((column) => column.visible !== false);
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
onMounted(() => {
|
||||
const settingInfoStore = useSettingInfo();
|
||||
if (settingInfoStore.rowOperate === RowOperate.RIGHT) {
|
||||
const operateColumns: Record<string, any>[] = [];
|
||||
const dataColumns: Record<string, any>[] = [];
|
||||
gridData.columns!.forEach((column) => {
|
||||
if (column['key'] === 'operation') {
|
||||
operateColumns.push(column);
|
||||
} else dataColumns.push(column);
|
||||
});
|
||||
gridData.columns.splice(0, gridData.columns.length);
|
||||
gridData.columns.push(...dataColumns);
|
||||
gridData.columns.push(...operateColumns.reverse());
|
||||
}
|
||||
|
||||
if (gridData.tableData) {
|
||||
dataList.value = gridData.tableData;
|
||||
gridData.total = dataList.value.length;
|
||||
pagination.value.total = gridData.total ?? 0;
|
||||
} else if (gridData.autoload !== false) {
|
||||
if (gridData.remotePage) gridQueryFind(pageData);
|
||||
else gridQueryList(pageData);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Toolbar :page-data="pageData" />
|
||||
<Table
|
||||
class="w-full"
|
||||
:rowKey="pageData.primaryKey"
|
||||
:data-source="dataList"
|
||||
:loading="gridData.isGridLoading"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
:row-selection="rowSelection"
|
||||
:scroll="{ x: 700, y: 1000 }"
|
||||
:size="gridData.tableSize"
|
||||
bordered
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template #bodyCell="bodyCell">
|
||||
<slot name="bodyCell" :column="bodyCell?.column" :record="bodyCell?.record"></slot>
|
||||
<template v-if="bodyCell?.column.dataIndex === 'enabled' || bodyCell?.column.dataIndex === 'disabled'">
|
||||
<Switch
|
||||
v-model:checked="bodyCell.record.Enabled"
|
||||
:data="OPTIONS.EnableDisable"
|
||||
@click="gridStatusUpdate(pageData, bodyCell.record)"
|
||||
:disabled="bodyCell?.column.dataIndex === 'disabled'"
|
||||
:class="['w-[58px]']"
|
||||
:loading="bodyCell?.record.isLoading"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="bodyCell?.column.dataIndex === 'operation'">
|
||||
<slot name="operate" :record="bodyCell?.record">
|
||||
<TableOperate :record="bodyCell?.record" :tools="gridData.operates" :page-data="pageData">
|
||||
</TableOperate>
|
||||
</slot>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</template>
|
|
@ -0,0 +1,134 @@
|
|||
<script setup lang="ts" generic="T">
|
||||
import { reactive, watch } from 'vue';
|
||||
import {
|
||||
ButtonTool,
|
||||
PageData,
|
||||
getToolGroup,
|
||||
getToolVisible,
|
||||
getToolStatus,
|
||||
onToolClicked,
|
||||
onGridRowEdit,
|
||||
onGridRowDelete,
|
||||
} from '@skyfox2000/webbase';
|
||||
import { ConfigProvider, Button, Space, DropdownButton, Menu, MenuItem, Popconfirm } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* 数据行记录
|
||||
*/
|
||||
record: Record<string, any>;
|
||||
/**
|
||||
* 页面数据
|
||||
*/
|
||||
pageData: PageData<T>;
|
||||
}>();
|
||||
|
||||
const pageData = props.pageData;
|
||||
const gridData = pageData.grid!;
|
||||
|
||||
/// 兼容其它按钮操作参数
|
||||
const defaultOperates: ButtonTool[] = [
|
||||
{
|
||||
key: 'Edit',
|
||||
label: '编辑',
|
||||
type: 'primary',
|
||||
visible: true,
|
||||
disabled: props.record.Enabled == 0,
|
||||
click: () => onGridRowEdit<T>(pageData, props.record as T),
|
||||
},
|
||||
{
|
||||
key: 'Delete',
|
||||
label: '删除',
|
||||
type: 'primary',
|
||||
visible: true,
|
||||
disabled: props.record.Enabled == 1,
|
||||
danger: true,
|
||||
confirm: true,
|
||||
confirmText: '是否删除此记录?',
|
||||
click: () => onGridRowDelete<T>(pageData, props.record as T),
|
||||
},
|
||||
];
|
||||
|
||||
const { buttons, menus } = getToolGroup(defaultOperates, 0, gridData.operates);
|
||||
|
||||
const Menus = reactive(menus);
|
||||
const Buttons = reactive(buttons);
|
||||
|
||||
watch(
|
||||
() => props.record.Enabled,
|
||||
() => {
|
||||
const { buttons, menus } = getToolGroup(defaultOperates, 0, gridData.operates);
|
||||
Buttons.splice(0, Buttons.length, ...buttons);
|
||||
Menus.splice(0, Menus.length, ...menus);
|
||||
},
|
||||
);
|
||||
|
||||
const disabled = (item: ButtonTool) => {
|
||||
switch (item.key) {
|
||||
case 'Edit':
|
||||
item.disabled = props.record.Enabled ? false : true;
|
||||
break;
|
||||
case 'Delete':
|
||||
item.disabled = props.record.Enabled ? true : false;
|
||||
break;
|
||||
}
|
||||
return getToolStatus(item, props.record);
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<ConfigProvider
|
||||
:theme="{
|
||||
token: {
|
||||
fontSize: 13,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<Space>
|
||||
<template v-for="item in Buttons" :key="item.key">
|
||||
<Popconfirm
|
||||
:disabled="!item.confirm"
|
||||
cancelText="否"
|
||||
okText="是"
|
||||
:title="item.confirmText"
|
||||
:okButtonProps="{ size: 'small' }"
|
||||
:cancelButtonProps="{ size: 'small' }"
|
||||
@confirm="onToolClicked(item, pageData, props.record, true)"
|
||||
>
|
||||
<Button
|
||||
:key="item.key"
|
||||
:type="item.type ?? 'text'"
|
||||
:danger="item.danger"
|
||||
v-if="getToolVisible(item)"
|
||||
:disabled="disabled(item)"
|
||||
@click="onToolClicked(item, pageData, props.record)"
|
||||
size="small"
|
||||
:style="{
|
||||
padding: item.type ?? '0px 4px',
|
||||
}"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</template>
|
||||
|
||||
<ConfigProvider :autoInsertSpaceInButton="false" v-if="record.Enabled == 1">
|
||||
<DropdownButton v-if="Menus.length > 0" size="small">
|
||||
更多
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<template v-for="item in Menus" :key="item.key">
|
||||
<MenuItem
|
||||
v-if="getToolVisible(item)"
|
||||
:disabled="disabled(item)"
|
||||
@click="onToolClicked(item, pageData, props.record)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</MenuItem>
|
||||
</template>
|
||||
</Menu>
|
||||
</template>
|
||||
</DropdownButton>
|
||||
</ConfigProvider>
|
||||
</Space>
|
||||
</ConfigProvider>
|
||||
</template>
|
|
@ -0,0 +1,143 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { onColumnVisibleChanged } from '@skyfox2000/webbase';
|
||||
import { VueDraggableNext as draggable } from 'vue-draggable-next';
|
||||
import { Checkbox } from 'ant-design-vue';
|
||||
import {
|
||||
PageData,
|
||||
getToolStatus,
|
||||
getToolByKey,
|
||||
getToolVisible,
|
||||
useToolFactory,
|
||||
onToolClicked,
|
||||
} from '@skyfox2000/webbase';
|
||||
import { Button, ToolIcon } from '@/components';
|
||||
import { Dropdown, Menu, MenuItem } from 'ant-design-vue';
|
||||
|
||||
// 定义组件映射
|
||||
// const componentMap: Record<string, any> = {
|
||||
// headset: () => import('./headset.vue'),
|
||||
// };
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* 页面主数据
|
||||
*/
|
||||
pageData: PageData<any>;
|
||||
}>();
|
||||
|
||||
const gridData = props.pageData.grid!;
|
||||
const { tools } = useToolFactory(props.pageData);
|
||||
|
||||
watch(
|
||||
() => gridData.selectable,
|
||||
(newVal) => {
|
||||
if (getToolByKey(tools.value, 'tool.export.excel.selected'))
|
||||
getToolByKey(tools.value, 'tool.export.excel.selected')!.disabled = !newVal;
|
||||
if (getToolByKey(tools.value, 'tool.export.pdf.selected'))
|
||||
getToolByKey(tools.value, 'tool.export.pdf.selected')!.disabled = !newVal;
|
||||
},
|
||||
);
|
||||
|
||||
const columns = gridData.columns;
|
||||
|
||||
// 添加拖拽相关的响应式数据
|
||||
const dragColumns = ref(columns);
|
||||
// 处理拖拽结束事件
|
||||
const onDragEnd = () => {
|
||||
// 更新列的顺序
|
||||
columns.splice(0, columns.length, ...dragColumns.value);
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="inline-flex [&>button]:ml-[-1px] first:[&>button]:ml-0">
|
||||
<template v-for="(item, index) in tools" :key="item.key">
|
||||
<Dropdown placement="bottomRight" class="p-0 rounded-none" v-if="getToolVisible(item) && item.dropdown">
|
||||
<template #overlay>
|
||||
<div class="min-w-[100px] bg-white rounded shadow-md p-4" :class="item.dropdownClass">
|
||||
<draggable
|
||||
v-if="item.dropdown === 'headset'"
|
||||
v-model="dragColumns"
|
||||
item-key="dataIndex"
|
||||
@end="onDragEnd"
|
||||
handle=".drag-handle"
|
||||
>
|
||||
<template v-for="element in dragColumns" :key="element.name">
|
||||
<div @click.stop class="flex items-center mb-2 last:mb-0 select-none">
|
||||
<span class="drag-handle mr-2 text-gray-400 hover:text-gray-600 cursor-move">⋮⋮</span>
|
||||
<Checkbox
|
||||
:checked="element.visible !== false"
|
||||
@change.stop.prevent="
|
||||
(e) => {
|
||||
onColumnVisibleChanged(element, e.target.checked);
|
||||
}
|
||||
"
|
||||
class="text-gray-700 hover:text-gray-900 select-none"
|
||||
>
|
||||
{{ element.title }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</template>
|
||||
<Button
|
||||
:class="[
|
||||
'px-[8px] py-[2px] relative border-[#ccc] bg-[#fcfcfc] rounded-none text-[#666] hover:z-10',
|
||||
index === 0 ? 'rounded-l-[5px]' : '',
|
||||
index === tools.length - 1 ? 'rounded-r-[5px]' : '',
|
||||
]"
|
||||
:disabled="getToolStatus(item)"
|
||||
:tiptext="item.label"
|
||||
@click="onToolClicked(item, pageData)"
|
||||
>
|
||||
<ToolIcon :icon="item.icon" class="w-[18px] h-[18.5px]" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Button
|
||||
v-else-if="!item.children && getToolVisible(item)"
|
||||
:class="[
|
||||
'px-[8px] py-[2px] relative border-[#ccc] bg-[#fcfcfc] rounded-none text-[#666] hover:z-10',
|
||||
index === 0 ? 'rounded-l-[5px]' : '',
|
||||
index === tools.length - 1 ? 'rounded-r-[5px]' : '',
|
||||
]"
|
||||
:disabled="getToolStatus(item)"
|
||||
:tiptext="item.label"
|
||||
@click="onToolClicked(item, pageData)"
|
||||
>
|
||||
<ToolIcon :icon="item.icon" class="w-[18px] h-[18.5px]" />
|
||||
</Button>
|
||||
<Dropdown placement="bottomRight" class="p-0 rounded-none" v-else-if="getToolVisible(item)">
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<MenuItem v-for="menu in item.children" :key="menu.key" :disabled="getToolStatus(menu)">
|
||||
{{ menu.label }}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
<Button
|
||||
:class="[
|
||||
'!w-[46px] px-[5px] py-[2px] relative border-[#ccc] bg-[#fcfcfc] rounded-none text-[#666] hover:z-10',
|
||||
index === 0 ? 'rounded-l-[5px]' : '',
|
||||
]"
|
||||
:disabled="getToolStatus(item)"
|
||||
:tiptext="item.label"
|
||||
:icon="item.icon"
|
||||
:iconProps="{ class: 'w-[19px] h-[19px]' }"
|
||||
@click="onToolClicked(item, pageData)"
|
||||
>
|
||||
<template #default>
|
||||
<ToolIcon icon="icon-down-arrow" class="w-[12px] h-[12px]" />
|
||||
</template>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</template>
|
||||
<!--
|
||||
导出Excel 基于表头导出
|
||||
导出PDF
|
||||
TODO:支持多模板导出选择
|
||||
表头设置
|
||||
TODO:增加保存功能,可设置个性化表头和多模式表头
|
||||
-->
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,72 @@
|
|||
<script setup lang="ts" generic="T">
|
||||
import { ButtonTool, PageData, getToolGroup, getToolStatus, onToolClicked, openNewForm } from '@skyfox2000/webbase';
|
||||
import { Dropdown, Menu, MenuItem, Space } from 'ant-design-vue';
|
||||
import { Button } from '@/components/common';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
// 定义异步组件
|
||||
const IconTool = defineAsyncComponent(() => import('./icontool.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* 页面主数据
|
||||
*/
|
||||
pageData: PageData<T>;
|
||||
}>();
|
||||
|
||||
const gridData = props.pageData.grid!;
|
||||
|
||||
const defaultButtons: ButtonTool[] = [
|
||||
{
|
||||
key: 'New',
|
||||
label: '新增',
|
||||
type: 'primary',
|
||||
icon: 'icon-new',
|
||||
danger: true,
|
||||
click: openNewForm,
|
||||
},
|
||||
];
|
||||
|
||||
const MaxButtonCount = 3;
|
||||
const { buttons, menus } = getToolGroup(
|
||||
defaultButtons,
|
||||
gridData.flat !== undefined ? gridData.flat : MaxButtonCount,
|
||||
gridData.buttons,
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex justify-between mb-[10px]">
|
||||
<Space>
|
||||
<Button
|
||||
v-for="item in buttons"
|
||||
:key="item.key"
|
||||
:type="item.type"
|
||||
:danger="item.danger"
|
||||
:disabled="getToolStatus(item)"
|
||||
:icon="item.icon"
|
||||
@click="onToolClicked(item, pageData)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Button>
|
||||
<Dropdown v-if="menus.length > 0">
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<MenuItem
|
||||
v-for="item in menus"
|
||||
:key="item.key"
|
||||
:disabled="getToolStatus(item)"
|
||||
@click="onToolClicked(item, pageData)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
<Button> 更多操作 </Button>
|
||||
</Dropdown>
|
||||
<!-- 空占位元素 -->
|
||||
<span v-if="buttons.length === 0 && menus.length === 0"></span>
|
||||
</Space>
|
||||
<Space class="mr-1">
|
||||
<component :is="IconTool" :page-data="pageData" />
|
||||
</Space>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { Result, Button } from 'ant-design-vue';
|
||||
import router from '@/router';
|
||||
const onClicked = () => {
|
||||
router.back();
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Result status="403" title="403" sub-title="您没有权限访问当前页面!">
|
||||
<template #extra>
|
||||
<Button type="primary" @click="onClicked">点击返回</Button>
|
||||
</template>
|
||||
</Result>
|
||||
</template>
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { Result, Button } from 'ant-design-vue';
|
||||
import router from '@/router';
|
||||
const onClicked = () => {
|
||||
router.back();
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Result status="404" title="404" sub-title="页面不存在或者没有权限访问页面!">
|
||||
<template #extra>
|
||||
<Button type="primary" @click="onClicked">点击返回</Button>
|
||||
</template>
|
||||
</Result>
|
||||
</template>
|
|
@ -0,0 +1,108 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { AutoComplete, AutoCompleteOption } from 'ant-design-vue';
|
||||
import {
|
||||
circleLoading,
|
||||
useInputFactory,
|
||||
OptionCommProps,
|
||||
OptionItemProps,
|
||||
onOptionChanged,
|
||||
loadOption,
|
||||
unloadOption,
|
||||
} from '@skyfox2000/webbase';
|
||||
import { ReqParams } from '@skyfox2000/fapi';
|
||||
|
||||
const props = defineProps({
|
||||
...OptionCommProps,
|
||||
autoload: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onsearch: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const inputFactory = useInputFactory();
|
||||
const { errClass, labelText } = inputFactory;
|
||||
|
||||
const emit = defineEmits(['change', 'update:label-info']);
|
||||
inputFactory.inputEmit = emit;
|
||||
|
||||
/**
|
||||
* 实际的选择项
|
||||
*/
|
||||
const selectOptions = ref<OptionItemProps[]>([]);
|
||||
|
||||
const onSearch = (value: string) => {
|
||||
selectOptions.value = [];
|
||||
let search_value = value.trim();
|
||||
let query: ReqParams = { Query: {} };
|
||||
if (props.onsearch) {
|
||||
props.onsearch(search_value, query);
|
||||
}
|
||||
|
||||
loadOption(true, props, selectOptions, inputFactory, query);
|
||||
};
|
||||
|
||||
const onSelected = (value: any, _option: any) => {
|
||||
onOptionChanged(props, value as number | string, selectOptions, inputFactory);
|
||||
|
||||
// const labelInfo: string[] = [];
|
||||
// if (typeof selectedOptions === 'object') selectedOptions = [selectedOptions];
|
||||
// selectedOptions.forEach((selectedOption: OptionItemProps) => {
|
||||
// labelInfo.push(selectedOption.label);
|
||||
// });
|
||||
|
||||
// emit('update:label-info', labelInfo);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (props.url && !props.url.fieldMap && !props.fieldMap) {
|
||||
props.url.fieldMap = {
|
||||
title: 'Name',
|
||||
label: 'Name',
|
||||
value: 'Id',
|
||||
key: 'Id',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
unloadOption(props, inputFactory);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="props.url && props.url.loading === true"
|
||||
class="absolute z-10 mt-[5px] mr-[10px] text-[#999] flex items-center"
|
||||
>
|
||||
<circleLoading class="text-[#555] mx-[5px] !ml-[10px] !w-4 !h-4" />
|
||||
<span>数据加载中...</span>
|
||||
</div>
|
||||
<AutoComplete
|
||||
:class="[errClass]"
|
||||
@search="onSearch"
|
||||
@select="onSelected"
|
||||
:placeholder="props.url && !props.url.loading ? '请输入并选择' + labelText : ''"
|
||||
v-bind="$attrs"
|
||||
:label-in-value="false"
|
||||
>
|
||||
<template v-for="item in selectOptions" :key="item.value">
|
||||
<AutoCompleteOption :value="item.value">
|
||||
{{ item.label }}
|
||||
</AutoCompleteOption>
|
||||
</template>
|
||||
</AutoComplete>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.error :deep(.ant-select-selector) {
|
||||
border-color: #ef4444;
|
||||
box-shadow: 0 0 3px 0 #ff4d4f;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,76 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import {
|
||||
circleLoading,
|
||||
useInputFactory,
|
||||
OptionCommProps,
|
||||
OptionItemProps,
|
||||
onOptionChanged,
|
||||
loadOption,
|
||||
unloadOption,
|
||||
} from '@skyfox2000/webbase';
|
||||
import { Cascader } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps(OptionCommProps);
|
||||
|
||||
const inputFactory = useInputFactory();
|
||||
const { errClass, labelText } = inputFactory;
|
||||
|
||||
const emit = defineEmits(['change', 'update:label-info']);
|
||||
inputFactory.inputEmit = emit;
|
||||
/**
|
||||
* 实际的选择项
|
||||
*/
|
||||
const selectOptions = ref<OptionItemProps[]>([]);
|
||||
|
||||
const onChanged = (value: any, selectedOptions: any) => {
|
||||
onOptionChanged(props, value as number | string, selectOptions, inputFactory);
|
||||
const labelInfo: string[] = [];
|
||||
if (selectedOptions) {
|
||||
selectedOptions.forEach((option: OptionItemProps) => {
|
||||
labelInfo.push(option.label);
|
||||
});
|
||||
}
|
||||
|
||||
emit('update:label-info', labelInfo);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (props.url && !props.url.fieldMap && !props.fieldMap) {
|
||||
props.url.fieldMap = {
|
||||
title: 'Name',
|
||||
label: 'Name',
|
||||
value: 'Id',
|
||||
key: 'Id',
|
||||
};
|
||||
}
|
||||
loadOption(props.autoload, props, selectOptions, inputFactory);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
unloadOption(props, inputFactory);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!selectOptions.length" class="absolute z-10 mt-[5px] mr-[10px] text-[#999] flex items-center">
|
||||
<circleLoading class="text-[#555] mx-[5px] !ml-[10px] !w-4 !h-4" />
|
||||
<span>数据加载中...</span>
|
||||
</div>
|
||||
<Cascader
|
||||
:options="selectOptions"
|
||||
:class="[errClass]"
|
||||
:allow-clear="true"
|
||||
:placeholder="selectOptions.length > 0 ? '请选择' + labelText : ''"
|
||||
@change="onChanged"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.error :deep(.ant-select-selector) {
|
||||
border-color: #ef444480;
|
||||
box-shadow: 0 0 3px 0 #ff4d4f;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,40 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { Checkbox, CheckboxGroup } from 'ant-design-vue';
|
||||
import { loadOption, onOptionChanged, OptionItemProps, unloadOption, useInputFactory } from "@skyfox2000/webbase";
|
||||
import { CheckboxValueType } from "ant-design-vue/es/checkbox/interface";
|
||||
import { OptionCommProps } from "@skyfox2000/webbase";
|
||||
|
||||
const props = defineProps(OptionCommProps);
|
||||
|
||||
const inputFactory = useInputFactory();
|
||||
|
||||
const checkboxOptions = ref<OptionItemProps[]>([]);
|
||||
|
||||
const onChanged = (e: CheckboxValueType[]) => {
|
||||
const checkedValue = e;
|
||||
onOptionChanged(props, checkedValue as (number | string)[], checkboxOptions, inputFactory);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.url && !props.url.fieldMap && !props.fieldMap) {
|
||||
props.url.fieldMap = {
|
||||
title: 'Name',
|
||||
label: 'Name',
|
||||
value: 'Id',
|
||||
key: 'Id',
|
||||
};
|
||||
}
|
||||
loadOption(props.autoload, props, checkboxOptions, inputFactory)
|
||||
});
|
||||
onUnmounted(() => {
|
||||
unloadOption(props, inputFactory);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<CheckboxGroup @change="onChanged" class="w-full" v-bind="$attrs">
|
||||
<Checkbox v-for="item in checkboxOptions" :key="item.value" :value="item.value"">
|
||||
{{ item.label }}
|
||||
</Checkbox>
|
||||
</CheckboxGroup>
|
||||
</template>
|
|
@ -0,0 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { DatePicker } from 'ant-design-vue';
|
||||
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
|
||||
import { useInputFactory } from '@skyfox2000/webbase';
|
||||
const props = defineProps<{
|
||||
valueFormat?: string;
|
||||
}>();
|
||||
|
||||
const inputFactory = useInputFactory();
|
||||
const { labelText, errClass } = inputFactory;
|
||||
|
||||
const dateFormat = ref<string>(props.valueFormat ?? 'YYYY-MM-DD');
|
||||
</script>
|
||||
<template>
|
||||
<DatePicker
|
||||
:class="
|
||||
errClass === 'error'
|
||||
? ['error', '!border-red-300', 'shadow-[0_0_3px_0px_#ff4d4f]']
|
||||
: ''
|
||||
"
|
||||
class="w-full"
|
||||
:placeholder="'请选择' + labelText"
|
||||
:locale="locale"
|
||||
:valueFormat="dateFormat"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,56 @@
|
|||
<script setup lang="ts">
|
||||
import { formValidate, useInputFactory } from '@skyfox2000/webbase';
|
||||
import { Input } from 'ant-design-vue';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const { editorData, labelText, errClass } = useInputFactory();
|
||||
const onBlur = () => {
|
||||
if (errClass.value && editorData.value) {
|
||||
/// 重新开始验证
|
||||
formValidate(editorData.value);
|
||||
}
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
value?: any;
|
||||
}>();
|
||||
const emit = defineEmits(['update:value']);
|
||||
const defaultValue = props.value;
|
||||
// 内部值,拦截外部传入的 value
|
||||
const innerValue = ref(props.value);
|
||||
|
||||
// 监听外部 value 的变化,更新内部值
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue) => {
|
||||
innerValue.value = newValue;
|
||||
},
|
||||
{ immediate: true }, // 立即同步初始值
|
||||
);
|
||||
|
||||
watch(
|
||||
() => innerValue.value,
|
||||
(newValue) => {
|
||||
emit('update:value', newValue); // 更新外部的 value
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const onClear = () => {
|
||||
if (innerValue.value === '') {
|
||||
emit('update:value', defaultValue);
|
||||
return;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Input
|
||||
:class="errClass === 'error' ? ['error', '!border-red-300', 'shadow-[0_0_3px_0px_#ff4d4f]'] : ''"
|
||||
v-model:value="innerValue"
|
||||
@change="onClear"
|
||||
:allow-clear="true"
|
||||
:placeholder="'请输入' + labelText"
|
||||
@blur="onBlur"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import { formValidate, useInputFactory } from '@skyfox2000/webbase';
|
||||
import { InputNumber } from 'ant-design-vue';
|
||||
|
||||
const { editorData, labelText, errClass } = useInputFactory();
|
||||
const onBlur = () => {
|
||||
if (errClass.value && editorData.value) {
|
||||
/// 重新开始验证
|
||||
formValidate(editorData.value);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<InputNumber
|
||||
:class="errClass === 'error' ? ['error', '!border-red-300', 'shadow-[0_0_3px_0px_#ff4d4f]'] : ''"
|
||||
@blur="onBlur"
|
||||
:allow-clear="false"
|
||||
:placeholder="'请输入' + labelText"
|
||||
class="w-[50%]"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import { formValidate, useInputFactory } from "@skyfox2000/webbase";
|
||||
import { InputPassword } from "ant-design-vue";
|
||||
|
||||
const { editorData, labelText, errClass } = useInputFactory();
|
||||
const onBlur = () => {
|
||||
if (errClass.value && editorData.value) {
|
||||
/// 重新开始验证
|
||||
formValidate(editorData.value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<InputPassword :class="errClass === 'error' ? ['error', '!border-red-300', 'shadow-[0_0_3px_0px_#ff4d4f]'] : ''"
|
||||
:allow-clear="true" :placeholder="'请输入' + labelText" @blur="onBlur" v-bind="$attrs" />
|
||||
</template>
|
|
@ -0,0 +1,72 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { Input, Button } from 'ant-design-vue';
|
||||
|
||||
interface ConfigItem {
|
||||
field: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
value: Record<string, string>;
|
||||
labelHolder?: string;
|
||||
valueHolder: string;
|
||||
}>();
|
||||
const emit = defineEmits(['update:config']);
|
||||
|
||||
const configList = ref<ConfigItem[]>([]);
|
||||
|
||||
const initConfigList = () => {
|
||||
configList.value = Object.entries(props.value).map(([field, value]) => ({
|
||||
field,
|
||||
value,
|
||||
}));
|
||||
};
|
||||
|
||||
watch(() => props.value, () => {
|
||||
initConfigList();
|
||||
}, { immediate: true });
|
||||
|
||||
const updateConfig = () => {
|
||||
const newConfig = configList.value.reduce((acc, item) => {
|
||||
if (item.field) {
|
||||
acc[item.field] = item.value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
emit('update:config', newConfig);
|
||||
};
|
||||
|
||||
const addNewLine = () => {
|
||||
configList.value.push({
|
||||
field: '',
|
||||
value: '',
|
||||
});
|
||||
};
|
||||
|
||||
const handleInputChange = () => {
|
||||
updateConfig();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div v-for="item in configList" :key="item.field" class="flex items-center gap-2">
|
||||
<div class="w-[33%]">
|
||||
<Input v-model:value="item.field" class="w-full" :placeholder="labelHolder || '配置名'"
|
||||
@change="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-[3%]">
|
||||
=
|
||||
</div>
|
||||
<div class="w-[64%]">
|
||||
<Input v-model:value="item.value" :placeholder="valueHolder" @change="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
<Button @click="addNewLine"
|
||||
class="mt-1 w-[80px] !text-[12px] text-[#666] bg-[#e6f7ff] border-[#b3e0ff] hover:bg-[#b3e0ff] hover:border-[#8abeff]"
|
||||
size="small">
|
||||
新增配置行
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,48 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { Radio, RadioChangeEvent, RadioGroup } from 'ant-design-vue';
|
||||
import { loadOption, onOptionChanged, OptionItemProps, unloadOption, useInputFactory } from '@skyfox2000/webbase';
|
||||
import { OptionCommProps } from '@skyfox2000/webbase';
|
||||
|
||||
const props = defineProps(OptionCommProps);
|
||||
|
||||
const inputFactory = useInputFactory();
|
||||
const { errClass } = inputFactory;
|
||||
const radioOptions = ref<OptionItemProps[]>([]);
|
||||
|
||||
const onChanged = (e: RadioChangeEvent) => {
|
||||
const checkedValue = e.target.value;
|
||||
onOptionChanged(props, checkedValue as number | string, radioOptions, inputFactory);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (props.url && !props.url.fieldMap && !props.fieldMap) {
|
||||
props.url.fieldMap = {
|
||||
title: 'Name',
|
||||
label: 'Name',
|
||||
value: 'Id',
|
||||
key: 'Id',
|
||||
};
|
||||
}
|
||||
loadOption(props.autoload, props, radioOptions, inputFactory);
|
||||
if (props.all) radioOptions.value.unshift({ label: '全部', value: undefined });
|
||||
});
|
||||
onUnmounted(() => {
|
||||
unloadOption(props, inputFactory);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<RadioGroup :autocheck="false" @change="onChanged" class="w-full flex align-items" v-bind="$attrs">
|
||||
<template v-for="item in radioOptions" :key="item.value">
|
||||
<Radio :value="item.value" :class="errClass === 'error' ? ['error', '!text-red-400'] : ''">
|
||||
{{ item.label }}
|
||||
</Radio>
|
||||
</template>
|
||||
</RadioGroup>
|
||||
</template>
|
||||
<style scoped>
|
||||
.error :deep(input + span) {
|
||||
border-color: #ff7171;
|
||||
box-shadow: 0 0 3px 0 #ff4d4f;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,73 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { RangePicker } from 'ant-design-vue';
|
||||
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
|
||||
import { useInputFactory } from '@skyfox2000/webbase';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
startDate?: string | null;
|
||||
endDate?: string | null;
|
||||
valueFormat?: string;
|
||||
}>(),
|
||||
{
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:startDate', value: string | null): void;
|
||||
(e: 'update:endDate', value: string | null): void;
|
||||
}>();
|
||||
|
||||
const inputFactory = useInputFactory();
|
||||
const { errClass } = inputFactory;
|
||||
|
||||
const dateFormat = computed(() => props.valueFormat);
|
||||
const rangeValue = computed(
|
||||
(): [Dayjs, Dayjs] | [string, string] | undefined => {
|
||||
const start = props.startDate;
|
||||
const end = props.endDate;
|
||||
if (!start || !end) return undefined;
|
||||
|
||||
try {
|
||||
const startDayjs = dayjs(start);
|
||||
const endDayjs = dayjs(end);
|
||||
if (!startDayjs.isValid() || !endDayjs.isValid()) return undefined;
|
||||
return [startDayjs, endDayjs];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const handleChange = (
|
||||
dates: [Dayjs, Dayjs] | [string, string] | null,
|
||||
dateStrings: [string, string],
|
||||
) => {
|
||||
if (!dates || !dateStrings || dateStrings.length !== 2) {
|
||||
emit('update:startDate', null);
|
||||
emit('update:endDate', null);
|
||||
return;
|
||||
}
|
||||
|
||||
emit('update:startDate', dateStrings[0] || null);
|
||||
emit('update:endDate', dateStrings[1] || null);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RangePicker
|
||||
:class="
|
||||
errClass === 'error'
|
||||
? ['error', '!border-red-300', 'shadow-[0_0_3px_0px_#ff4d4f]']
|
||||
: ''
|
||||
"
|
||||
class="w-full"
|
||||
:locale="locale"
|
||||
:value-format="dateFormat"
|
||||
:value="rangeValue"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,86 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { Select, SelectOption } from 'ant-design-vue';
|
||||
import type { DefaultOptionType, SelectValue } from 'ant-design-vue/es/select';
|
||||
import {
|
||||
circleLoading,
|
||||
useInputFactory,
|
||||
OptionCommProps,
|
||||
OptionItemProps,
|
||||
onOptionChanged,
|
||||
loadOption,
|
||||
unloadOption,
|
||||
} from '@skyfox2000/webbase';
|
||||
|
||||
const props = defineProps(OptionCommProps);
|
||||
|
||||
const inputFactory = useInputFactory();
|
||||
const { errClass, labelText } = inputFactory;
|
||||
|
||||
const emit = defineEmits(['change', 'update:label-info']);
|
||||
inputFactory.inputEmit = emit;
|
||||
/**
|
||||
* 实际的选择项
|
||||
*/
|
||||
const selectOptions = ref<OptionItemProps[]>([]);
|
||||
|
||||
const onChanged = (value: SelectValue, selectedOptions: DefaultOptionType | DefaultOptionType[]) => {
|
||||
onOptionChanged(props, value as number | string, selectOptions, inputFactory);
|
||||
|
||||
const labelInfo: string[] = [];
|
||||
if (typeof selectedOptions === 'object') selectedOptions = [selectedOptions];
|
||||
selectedOptions.forEach((selectedOption: OptionItemProps) => {
|
||||
labelInfo.push(selectedOption.label);
|
||||
});
|
||||
|
||||
emit('update:label-info', labelInfo);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (props.url && !props.url.fieldMap && !props.fieldMap) {
|
||||
props.url.fieldMap = {
|
||||
title: 'Name',
|
||||
label: 'Name',
|
||||
value: 'Id',
|
||||
key: 'Id',
|
||||
};
|
||||
}
|
||||
loadOption(props.autoload, props, selectOptions, inputFactory);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
unloadOption(props, inputFactory);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="props.url && props.url.loading === true"
|
||||
class="absolute z-10 mt-[5px] mr-[10px] text-[#999] flex items-center"
|
||||
>
|
||||
<circleLoading class="text-[#555] mx-[5px] !ml-[10px] !w-4 !h-4" />
|
||||
<span>数据加载中...</span>
|
||||
</div>
|
||||
<Select
|
||||
:class="[errClass]"
|
||||
@change="onChanged"
|
||||
:placeholder="props.url && !props.url.loading ? '请选择' + labelText : ''"
|
||||
v-bind="$attrs"
|
||||
:label-in-value="false"
|
||||
>
|
||||
<template v-for="item in selectOptions" :key="item.value">
|
||||
<SelectOption :value="item.value">
|
||||
{{ item.label }}
|
||||
</SelectOption>
|
||||
</template>
|
||||
</Select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.error :deep(.ant-select-selector) {
|
||||
border-color: #ef4444;
|
||||
box-shadow: 0 0 3px 0 #ff4d4f;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,59 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { formValidate, useInputFactory, OptionItemProps, loadOption, unloadOption } from '@skyfox2000/webbase';
|
||||
import { Switch } from 'ant-design-vue';
|
||||
import message from 'vue-m-message';
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 选择项数据
|
||||
*/
|
||||
data: {
|
||||
type: Array as PropType<Record<string, any>[]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 实际的选择项
|
||||
*/
|
||||
const switchOptions = ref<OptionItemProps[]>([]);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', checked: boolean | string | number): void;
|
||||
}>();
|
||||
|
||||
const { editorData, errClass } = useInputFactory();
|
||||
const onChange = (checked: boolean | string | number) => {
|
||||
if (errClass.value && editorData.value) {
|
||||
/// 重新开始验证
|
||||
formValidate(editorData.value);
|
||||
}
|
||||
emit('change', checked);
|
||||
};
|
||||
onMounted(() => {
|
||||
if (!props.data || props.data.length != 2) {
|
||||
console.error('Switch组件: ', props.data);
|
||||
message.error('Switch组件必须有且只有两个选项');
|
||||
return;
|
||||
}
|
||||
loadOption(false, props, switchOptions);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
unloadOption(props);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Switch
|
||||
v-if="switchOptions.length === 2"
|
||||
:class="[errClass === 'error' ? 'error !border-red-300 shadow-[0_0_3px_0px_#ff4d4f]' : '', 'w-[58px]']"
|
||||
:checkedChildren="switchOptions[0].label"
|
||||
:checkedValue="switchOptions[0].value"
|
||||
:unCheckedChildren="switchOptions[1].label"
|
||||
:unCheckedValue="switchOptions[1].value"
|
||||
@change="onChange"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import { formValidate, useInputFactory } from '@skyfox2000/webbase';
|
||||
import { Textarea } from 'ant-design-vue';
|
||||
|
||||
const { editorData, labelText, errClass } = useInputFactory();
|
||||
const onBlur = () => {
|
||||
if (errClass.value && editorData.value) {
|
||||
/// 重新开始验证
|
||||
formValidate(editorData.value);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Textarea
|
||||
:class="errClass === 'error' ? ['error', '!border-red-300', 'shadow-[0_0_3px_0px_#ff4d4f]'] : ''"
|
||||
:allow-clear="true"
|
||||
:placeholder="'请输入' + labelText"
|
||||
@blur="onBlur"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,43 @@
|
|||
export { default as Button } from './common/button/index.vue';
|
||||
export { default as Appicon } from './common/icon/appicon.vue';
|
||||
export { default as Fullscreen } from './common/icon/fullscreen.vue';
|
||||
export { default as Helper } from './common/icon/helper.vue';
|
||||
export { default as Icon } from './common/icon/index.vue';
|
||||
export { default as LayoutIcon } from './common/icon/layoutIcon.vue';
|
||||
export { default as ToolIcon } from './common/icon/toolIcon.vue';
|
||||
export { default as Tooltip } from './common/tooltip/index.vue';
|
||||
export { default as Dialog } from './content/dialog/index.vue';
|
||||
export { default as UploadForm } from './content/dialog/uploadForm.vue';
|
||||
export { default as UploadList } from './content/dialog/uploadList.vue';
|
||||
export { default as Drawer } from './content/drawer/index.vue';
|
||||
export { default as FormItem } from './content/form/formItem.vue';
|
||||
export { default as Form } from './content/form/index.vue';
|
||||
export { default as Search } from './content/search/index.vue';
|
||||
export { default as SearchItem } from './content/search/searchItem.vue';
|
||||
export { default as Table } from './content/table/index.vue';
|
||||
export { default as TableOperate } from './content/table/tableOperate.vue';
|
||||
export { default as Icontool } from './content/toolbar/icontool.vue';
|
||||
export { default as Toolbar } from './content/toolbar/index.vue';
|
||||
export { default as Error403 } from './error/error403.vue';
|
||||
export { default as Error404 } from './error/error404.vue';
|
||||
export { default as AutoComplete } from './form/autoComplete/index.vue';
|
||||
export { default as Cascader } from './form/cascader/index.vue';
|
||||
export { default as Checkbox } from './form/checkbox/index.vue';
|
||||
export { default as DatePicker } from './form/datePicker/index.vue';
|
||||
export { default as Input } from './form/input/index.vue';
|
||||
export { default as InputNumber } from './form/input/inputNumber.vue';
|
||||
export { default as InputPassword } from './form/input/inputPassword.vue';
|
||||
export { default as PropEditor } from './form/propEditor/index.vue';
|
||||
export { default as Radio } from './form/radio/index.vue';
|
||||
export { default as RangePicker } from './form/rangePicker/index.vue';
|
||||
export { default as Select } from './form/select/index.vue';
|
||||
export { default as Switch } from './form/switch/index.vue';
|
||||
export { default as Textarea } from './form/textarea/index.vue';
|
||||
export { default as Breadcrumb } from './layout/breadcrumb/index.vue';
|
||||
export { default as Content } from './layout/content/index.vue';
|
||||
export { default as Datetime } from './layout/datetime/index.vue';
|
||||
export { default as HeaderExits } from './layout/header/headerExits.vue';
|
||||
export { default as Header } from './layout/header/index.vue';
|
||||
export { default as Menu } from './layout/menu/index.vue';
|
||||
export { default as MenuTabs } from './layout/menu/menuTabs.vue';
|
||||
export { default as BasicLayout } from './layout/page/basicLayout.vue';
|
|
@ -0,0 +1,38 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { Breadcrumb, theme } from 'ant-design-vue';
|
||||
import { LayoutIcon } from '@/components';
|
||||
import { BreadcrumbRoute, showBreadcrumb, crumbs, usePageInfo } from '@skyfox2000/webbase';
|
||||
|
||||
const { useToken } = theme;
|
||||
const { token } = useToken();
|
||||
|
||||
const pageInfoStore = usePageInfo();
|
||||
watch(
|
||||
() => pageInfoStore.TabActive,
|
||||
() => showBreadcrumb(),
|
||||
);
|
||||
onMounted(() => {
|
||||
showBreadcrumb();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="ml-5 h-fit p-0 flex items-center justify-between"
|
||||
:style="{
|
||||
backgroundColor: token.colorBgContainer,
|
||||
}"
|
||||
>
|
||||
<LayoutIcon icon="icon-home" class="w-[15px]" />
|
||||
<span class="leading-[2.5] mx-[6px] text-[rgba(0,0,0,0.45)]">></span>
|
||||
<Breadcrumb :routes="crumbs" separator="">
|
||||
<template #itemRender="{ route }: { route: BreadcrumbRoute }">
|
||||
<span class="text-xs leading-[3]">{{ route.breadcrumbName }}</span>
|
||||
<LayoutIcon :icon="route.icon" fontsize="15px" />
|
||||
<span v-if="route.index! < crumbs.length - 1" class="leading-[2.5] mx-[6px] text-[rgba(0,0,0,0.45)]"
|
||||
>></span
|
||||
>
|
||||
</template>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts" setup>
|
||||
import { LayoutContent, theme } from 'ant-design-vue';
|
||||
const { useToken } = theme;
|
||||
const { token } = useToken();
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="relative h-[calc(100vh-80px)] overflow-y-auto">
|
||||
<LayoutContent class="m-[10px] p-[10px] h-fit min-h-[calc(100vh-100px)]" :style="{
|
||||
backgroundColor: token.colorBgContainer,
|
||||
}">
|
||||
<slot></slot>
|
||||
</LayoutContent>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
const DateTime = ref("")
|
||||
onMounted(() => {
|
||||
setInterval(() => {
|
||||
const curTime = new Date()
|
||||
const options: any = { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' };
|
||||
const formattedTime = curTime.toLocaleString(undefined, options).replace(/\//g, '-').replace(',', '');
|
||||
DateTime.value = curTime.getFullYear() + "-" + formattedTime
|
||||
}, 1000);
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="font-['Courier'] text-[#666]">{{ DateTime }}</div>
|
||||
</template>
|
|
@ -0,0 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Modal, Flex } from 'ant-design-vue';
|
||||
import { useUserInfo } from '@skyfox2000/webbase';
|
||||
import { LayoutIcon } from '@/components';
|
||||
|
||||
const userInfoStore = useUserInfo();
|
||||
|
||||
const open = ref(false);
|
||||
const confirmExit = () => {
|
||||
userInfoStore.logout();
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<LayoutIcon icon="icon-logout" @click="open = true" class="cursor-pointer w-5 h-5" />
|
||||
<Modal v-model:open="open" title="确定退出?" ok-text="确定" cancel-text="取消" :width="380" @ok="confirmExit">
|
||||
<Flex align="center" justify="flex-start" :style="{ padding: '0 32px', margin: '20px 0' }">
|
||||
<LayoutIcon icon="icon-question-circle" color="orange" class="w-[60px] h-[60px]" />
|
||||
<div style="margin: 0 0 0 20px; font-weight: 400; font-size: 16px">是否退出系统,<br />清除用户缓存信息?</div>
|
||||
</Flex>
|
||||
</Modal>
|
||||
</template>
|
|
@ -0,0 +1,43 @@
|
|||
<script lang="ts" setup>
|
||||
import { LayoutHeader, Avatar, theme, Space } from 'ant-design-vue';
|
||||
import { useSettingInfo } from '@skyfox2000/webbase';
|
||||
import { LayoutIcon } from '@/components';
|
||||
import Breadcrumb from '../breadcrumb/index.vue';
|
||||
import HeaderExits from './headerExits.vue';
|
||||
|
||||
const { useToken } = theme;
|
||||
const { token } = useToken();
|
||||
|
||||
const settingInfoStore = useSettingInfo();
|
||||
const onCollapseClick = () => {
|
||||
settingInfoStore.setMenuCollapse(!settingInfoStore.menuCollapse);
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<LayoutHeader
|
||||
class="w-full relative z-[1] shadow-[0_-3px_6px_#000] py-0 flex items-center justify-between"
|
||||
:style="{
|
||||
height: '40px',
|
||||
lineHeight: '1',
|
||||
paddingLeft: '10px',
|
||||
paddingRight: '10px',
|
||||
backgroundColor: token.colorBgContainer,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<LayoutIcon
|
||||
icon="icon-menu"
|
||||
class="w-[18px] h-[18px] cursor-pointer"
|
||||
:angle="settingInfoStore.menuCollapse ? 90 : 0"
|
||||
@click="onCollapseClick"
|
||||
/>
|
||||
<Breadcrumb></Breadcrumb>
|
||||
</div>
|
||||
<div>
|
||||
<Space size="middle" class="flex items-center">
|
||||
<Avatar class="avatar" :style="{ backgroundColor: '#f56a00', fontSize: '14px' }" :size="24"> U </Avatar>
|
||||
<HeaderExits />
|
||||
</Space>
|
||||
</div>
|
||||
</LayoutHeader>
|
||||
</template>
|
|
@ -0,0 +1,69 @@
|
|||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { ItemType, Menu } from 'ant-design-vue';
|
||||
import type { MenuProps } from 'ant-design-vue';
|
||||
import { routes } from '@/router';
|
||||
import { AppRouter, initMenu, useSettingInfo, useAppInfo, usePageInfo } from '@skyfox2000/webbase';
|
||||
|
||||
import { LayoutIcon } from '@/components';
|
||||
|
||||
const openKeys = ref<string[]>([]);
|
||||
const selectedKeys = ref<string[]>([]);
|
||||
|
||||
// 转换为树形结构
|
||||
const menuData: Array<ItemType> = reactive([]);
|
||||
|
||||
const pageInfoStore = usePageInfo();
|
||||
const onMenuClick: MenuProps['onClick'] = (menuInfo) => {
|
||||
useAppInfo().push(menuInfo.key.toString());
|
||||
};
|
||||
|
||||
const settingInfoStore = useSettingInfo();
|
||||
const showOpenKeys = ref<string[]>([]);
|
||||
const activeMenu = () => {
|
||||
let subPath = pageInfoStore.TabActive;
|
||||
const paths = subPath.split('/');
|
||||
paths.pop();
|
||||
openKeys.value = [paths.join('/')];
|
||||
if (!settingInfoStore.menuCollapse) {
|
||||
showOpenKeys.value = [paths.join('/')];
|
||||
}
|
||||
selectedKeys.value = [subPath];
|
||||
};
|
||||
|
||||
watch(
|
||||
() => settingInfoStore.menuCollapse,
|
||||
(newVal) => {
|
||||
if (!newVal) {
|
||||
showOpenKeys.value = [];
|
||||
nextTick(() => {
|
||||
showOpenKeys.value = [...openKeys.value];
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => pageInfoStore.TabActive,
|
||||
() => {
|
||||
activeMenu();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
initMenu<ItemType>(routes, menuData, LayoutIcon, { class: '!w-5 !h-5' });
|
||||
pageInfoStore.setTabActive(AppRouter.currentRoute.value.path);
|
||||
|
||||
activeMenu();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Menu
|
||||
v-model:openKeys="showOpenKeys"
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
theme="dark"
|
||||
:items="menuData"
|
||||
@click="onMenuClick"
|
||||
>
|
||||
</Menu>
|
||||
</template>
|
|
@ -0,0 +1,55 @@
|
|||
<script setup lang="ts">
|
||||
import { Tabs, TabPane, theme } from 'ant-design-vue';
|
||||
import { useAppInfo, usePageInfo } from '@skyfox2000/webbase';
|
||||
|
||||
import { LayoutIcon, Tooltip } from '@/components';
|
||||
|
||||
const { useToken } = theme;
|
||||
const { token } = useToken();
|
||||
|
||||
const pageInfoStore = usePageInfo();
|
||||
|
||||
const onTabClicked = (tabKey: any) => {
|
||||
useAppInfo().push(tabKey as string);
|
||||
};
|
||||
|
||||
const closeTab = (key: string) => {
|
||||
pageInfoStore.removeTabPane(key);
|
||||
useAppInfo().push(pageInfoStore.TabActive);
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div :style="{ height: '38px', backgroundColor: token.colorBgBase }">
|
||||
<Tabs
|
||||
:activeKey="pageInfoStore.TabActive"
|
||||
hide-add
|
||||
size="small"
|
||||
:tabBarStyle="{ padding: '0 20px' }"
|
||||
@tabClick="onTabClicked"
|
||||
>
|
||||
<TabPane v-for="pane in pageInfoStore.TabPanes" :key="pane.key">
|
||||
<template #tab>
|
||||
<div class="flex items-center">
|
||||
<span class="flex">
|
||||
{{ pane.title }}
|
||||
</span>
|
||||
<Tooltip title="关闭" placement="top" size="small">
|
||||
<div
|
||||
class="inline-block mx-auto relative flex items-center"
|
||||
v-if="pageInfoStore.TabPanes.length > 1 && pane.closable"
|
||||
@click.stop="closeTab(pane.key)"
|
||||
>
|
||||
<LayoutIcon
|
||||
icon="icon-add"
|
||||
:angle="45"
|
||||
class="w-[15px] h-[15px] ml-1"
|
||||
:tipcolor="token.colorBgSpotlight"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,54 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { Layout, LayoutSider } from 'ant-design-vue';
|
||||
import Tooltip from '../../common/tooltip/index.vue';
|
||||
import Icon from '../../common/icon/index.vue';
|
||||
import Menu from '../menu/index.vue';
|
||||
import Header from '../header/index.vue';
|
||||
import MenuTabs from '../menu/menuTabs.vue';
|
||||
|
||||
import { useAppInfo, usePageInfo, useSettingInfo } from '@skyfox2000/webbase';
|
||||
|
||||
const appInfoStore = useAppInfo();
|
||||
const settingInfoStore = useSettingInfo();
|
||||
const pageInfoStore = usePageInfo();
|
||||
|
||||
const bodyClass = ref('h-[calc(100vh-80px)]');
|
||||
watch(
|
||||
() => settingInfoStore.fullscreen,
|
||||
(newVal) => {
|
||||
bodyClass.value = newVal ? 'h-[calc(100vh-40px)]' : 'h-[calc(100vh-80px)]';
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<Layout class="h-screen">
|
||||
<LayoutSider
|
||||
class="overflow-auto h-screen left-0 top-0 bottom-0"
|
||||
v-model:collapsed="settingInfoStore.menuCollapse"
|
||||
collapsible
|
||||
v-if="!settingInfoStore.fullscreen"
|
||||
>
|
||||
<div
|
||||
class="h-[40px] max-h-[40px] bg-[rgba(240,240,240,0.2)] flex flex-nowrap items-center justify-center text-white font-bold text-lg overflow-hidden text-ellipsis"
|
||||
>
|
||||
<Tooltip :title="settingInfoStore.menuCollapse ? appInfoStore.appInfo.Name : ''" placement="right">
|
||||
<Icon :icon="appInfoStore.appInfo.Icon" fontsize="30px" size="26px" />
|
||||
</Tooltip>
|
||||
<span v-if="!settingInfoStore.menuCollapse" class="ml-[10px]">{{ appInfoStore.appInfo.Name }}</span>
|
||||
</div>
|
||||
<Menu></Menu>
|
||||
</LayoutSider>
|
||||
<Layout class="overflow-y-auto block">
|
||||
<Header v-if="!settingInfoStore.fullscreen"></Header>
|
||||
<MenuTabs v-if="pageInfoStore.TabEnabled"></MenuTabs>
|
||||
<div class="relative overflow-y-auto" :class="bodyClass">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<keep-alive :include="appInfoStore.CachedComponents" :exclude="appInfoStore.ExcludeComponents">
|
||||
<component :is="appInfoStore.cacheComponent(Component, route)" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</template>
|
|
@ -0,0 +1,67 @@
|
|||
import '@/assets/styles/global.css';
|
||||
import 'vue-m-message/dist/style.css';
|
||||
|
||||
import Message from 'vue-m-message';
|
||||
|
||||
import { createApp } from 'vue';
|
||||
//引入状态管理
|
||||
import { createPinia } from 'pinia';
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||
|
||||
import { initMainAppData, isMicroApp } from '@skyfox2000/microbase';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
dayjs.locale('zh-cn');
|
||||
|
||||
import App from '@/App.vue';
|
||||
|
||||
//引入路由
|
||||
import router from '@/router';
|
||||
import { appList, appRoutes } from '@/router/app-routes';
|
||||
|
||||
import { initValidate, useUserInfo, useHostInfo, useAppInfo } from '@skyfox2000/webbase';
|
||||
import { SERVER_HOST } from '@skyfox2000/fapi';
|
||||
|
||||
const initializeApp = async () => {
|
||||
initValidate();
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
app.use(pinia);
|
||||
app.use(Message);
|
||||
|
||||
const userInfoStore = useUserInfo();
|
||||
userInfoStore.init();
|
||||
|
||||
if (!isMicroApp()) {
|
||||
// 本地应用,使用本地配置加载方式
|
||||
const hostInfoStore = useHostInfo();
|
||||
await hostInfoStore.loadHostInfo(import.meta.env.VITE_SITEHOST_API, import.meta.env.VITE_SITEHOST);
|
||||
} else {
|
||||
SERVER_HOST.TOOL_ICONS = import.meta.env.VITE_TOOL_ICONS;
|
||||
SERVER_HOST.MICROLAYOUT_ICONS = import.meta.env.VITE_MICROLAYOUT_ICONS;
|
||||
}
|
||||
|
||||
const appInfoStore = useAppInfo();
|
||||
await appInfoStore.loadAppList(appList);
|
||||
await appInfoStore.setAppRoutes(appRoutes);
|
||||
|
||||
app.use(router);
|
||||
app.mount('#app');
|
||||
|
||||
if (import.meta.env.VITE_LOGIN_TEST == 'true') {
|
||||
await userInfoStore.login({
|
||||
UserName: import.meta.env.VITE_LOGIN_TEST_USERNAME,
|
||||
UserPass: import.meta.env.VITE_LOGIN_TEST_PASSWORD,
|
||||
});
|
||||
} else {
|
||||
}
|
||||
};
|
||||
|
||||
if (isMicroApp()) {
|
||||
initMainAppData();
|
||||
initializeApp();
|
||||
} else {
|
||||
initializeApp();
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import { AppAction, AppInfo, AppSource } from '@skyfox2000/microbase';
|
||||
import '/public/layout/appicons.js';
|
||||
import { parseIcons } from '@skyfox2000/webbase';
|
||||
|
||||
parseIcons({
|
||||
iconUrl: '',
|
||||
monoColor: false,
|
||||
});
|
||||
const BasicLayout = () => import('@/components/layout/page/basicLayout.vue');
|
||||
|
||||
export const Application: AppInfo = {
|
||||
Id: 'hospital',
|
||||
Name: '后台管理',
|
||||
AppCode: 'hospital',
|
||||
Version: '1.0',
|
||||
Host: '/',
|
||||
Source: AppSource.Manual,
|
||||
Action: AppAction.App,
|
||||
Default: true,
|
||||
Icon: 'sym-app-setting',
|
||||
Enabled: 1,
|
||||
};
|
||||
|
||||
export const appList = [Application];
|
||||
|
||||
export const appRoutes = [
|
||||
{
|
||||
path: '/business',
|
||||
name: '业务管理',
|
||||
component: BasicLayout,
|
||||
icon: 'icon-home',
|
||||
children: [
|
||||
{
|
||||
path: 'order',
|
||||
name: '订单管理',
|
||||
component: () => import('@/views/business/order/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'priceqr',
|
||||
name: '价码管理',
|
||||
component: () => import('@/views/business/priceqr/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'helper',
|
||||
name: '取样帮手',
|
||||
component: () => import('@/views/business/helper/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
name: '系统管理',
|
||||
component: BasicLayout,
|
||||
icon: 'icon-setting',
|
||||
children: [
|
||||
{
|
||||
path: 'member',
|
||||
name: '会员管理',
|
||||
component: () => import('@/views/system/member/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'hospital',
|
||||
name: '医院管理',
|
||||
component: () => import('@/views/system/hospital/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'doctor',
|
||||
name: '医生管理',
|
||||
component: () => import('@/views/system/doctor/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'account',
|
||||
name: '用户管理',
|
||||
component: () => import('@/views/system/account/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -0,0 +1,60 @@
|
|||
import { AppRouter, routes, useUserInfo } from '@skyfox2000/webbase';
|
||||
|
||||
const BasicLayout = () => import('@/components/layout/page/basicLayout.vue');
|
||||
|
||||
const NotFound404 = () => import('@/components/error/error404.vue');
|
||||
|
||||
/**
|
||||
* 默认路由
|
||||
*/
|
||||
const defaultRoutes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
redirect: '/business/order',
|
||||
beforeEnter: () => {
|
||||
const userInfoStore = useUserInfo();
|
||||
if (!userInfoStore.isLogin) {
|
||||
AppRouter.replace('/login');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
beforeEnter: () => {
|
||||
const userInfoStore = useUserInfo();
|
||||
if (userInfoStore.isLogin) {
|
||||
AppRouter.replace('/business/order');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
// 404 路由
|
||||
{
|
||||
path: '/error', // 使用通配符 :catchAll 匹配所有未匹配到的路由
|
||||
name: 'NotFound',
|
||||
component: BasicLayout,
|
||||
children: [
|
||||
{
|
||||
path: '404',
|
||||
name: '404',
|
||||
component: NotFound404,
|
||||
meta: { keepAlive: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
defaultRoutes.forEach((route) => {
|
||||
routes.push(route);
|
||||
AppRouter.addRoute(route);
|
||||
});
|
||||
|
||||
export { routes };
|
||||
|
||||
export default AppRouter;
|
|
@ -0,0 +1,343 @@
|
|||
/* eslint-disable */
|
||||
|
||||
/* prettier-ignore */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// Generated by unplugin-auto-import
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* 取样帮手配置
|
||||
*/
|
||||
interface SamplingHelperEntity extends import('src/views/business/helper/model')['SamplingHelperEntity'] {
|
||||
/**
|
||||
* 配置ID
|
||||
*/
|
||||
Id: string | null;
|
||||
/**
|
||||
* 医生
|
||||
*/
|
||||
DoctorId: string | null;
|
||||
/**
|
||||
* 地区信息
|
||||
*/
|
||||
LocationInfo?: { [key: string]: any };
|
||||
/**
|
||||
* 取样流程
|
||||
*/
|
||||
Process: string | null;
|
||||
/**
|
||||
* 启用状态
|
||||
*/
|
||||
Enabled: number;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
CreateTime?: string | null;
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
UpdateTime?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单信息
|
||||
*/
|
||||
interface OrderEntity extends import('src/views/business/order/model')['OrderEntity'] {
|
||||
/**
|
||||
* 订单ID
|
||||
*/
|
||||
Id: string | null;
|
||||
/**
|
||||
* 所属会员
|
||||
*/
|
||||
MemberId: string | null;
|
||||
/**
|
||||
* 所属会员
|
||||
*/
|
||||
MemberName: string | null;
|
||||
/**
|
||||
* 价码ID
|
||||
*/
|
||||
PriceId: string | null;
|
||||
/**
|
||||
* 价码
|
||||
*/
|
||||
PriceCode: string | null;
|
||||
/**
|
||||
* 产品名称
|
||||
*/
|
||||
ProductName: string | null;
|
||||
/**
|
||||
* 会员地址
|
||||
*/
|
||||
Address: string | null;
|
||||
/**
|
||||
* 订单快递号
|
||||
*/
|
||||
ExpressNumber?: string | null;
|
||||
/**
|
||||
* 快递地址信息
|
||||
*/
|
||||
ExpressInfo?: { [key: string]: any };
|
||||
/**
|
||||
* 检查报告
|
||||
*/
|
||||
ReportFileUrl?: string | null;
|
||||
/**
|
||||
* 订单状态
|
||||
*/
|
||||
Status: string | null;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
Remark?: string | null;
|
||||
/**
|
||||
* 启用状态
|
||||
*/
|
||||
Enabled: number;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
CreateTime?: string | null;
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
UpdateTime?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 价码信息
|
||||
*/
|
||||
interface PriceQREntity extends import('src/views/business/priceqr/model')['PriceQREntity'] {
|
||||
/**
|
||||
* 价格ID
|
||||
*/
|
||||
Id: string | null;
|
||||
/**
|
||||
* 价格码
|
||||
*/
|
||||
Code: string | null;
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
Name: string | null;
|
||||
/**
|
||||
* 单价
|
||||
*/
|
||||
Price: number | null;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
Remark?: string | null;
|
||||
/**
|
||||
* 启用状态
|
||||
*/
|
||||
Enabled: number;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
CreateTime?: string | null;
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
UpdateTime?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号
|
||||
*/
|
||||
interface AccountEntity extends import('src/views/system/account/model')['AccountEntity'] {
|
||||
/**
|
||||
* 帐号类型(Admin管理员/User操作员)
|
||||
*/
|
||||
AccountType: string | null;
|
||||
/**
|
||||
* 所属租户
|
||||
*/
|
||||
ClientId?: string | null;
|
||||
/**
|
||||
* 主部门
|
||||
*/
|
||||
DepartId?: string | null;
|
||||
/**
|
||||
* 启用状态
|
||||
*/
|
||||
Enabled: number;
|
||||
/**
|
||||
* 主键Id
|
||||
*/
|
||||
Id: string | null;
|
||||
/**
|
||||
* 主职位
|
||||
*/
|
||||
JobTitleId?: string | null;
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
Name: string | null;
|
||||
/**
|
||||
* 密码时间
|
||||
*/
|
||||
PassTime?: string | null;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
Remark?: string | null;
|
||||
/**
|
||||
* 审核状态
|
||||
*/
|
||||
State?: string | null;
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
Tags?: string[];
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
UpdateTime?: string | null;
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
UpdateUser?: string | null;
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
UserCode: string | null;
|
||||
/**
|
||||
* 用户级别(Platform平台/Client租户/Member会员)
|
||||
*/
|
||||
UserLevel: string | null;
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
UserProps?: { [key: string]: any };
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
UserPwd?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 医生账号
|
||||
*/
|
||||
interface DoctorEntity extends import('src/views/system/doctor/model')['DoctorEntity'] {
|
||||
/**
|
||||
* 医生ID
|
||||
*/
|
||||
Id: string | null;
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
Mobile: string | null;
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
Name: string | null;
|
||||
/**
|
||||
* 所属医院
|
||||
*/
|
||||
HospitalId: string | null;
|
||||
/**
|
||||
* 启用状态
|
||||
*/
|
||||
Enabled: number;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
CreateTime?: string | null;
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
UpdateTime?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 医院信息
|
||||
*/
|
||||
interface HospitalEntity extends import('src/views/system/hospital/model')['HospitalEntity'] {
|
||||
/**
|
||||
* 医院ID
|
||||
*/
|
||||
Id: string | null;
|
||||
/**
|
||||
* 医院名称
|
||||
*/
|
||||
Name: string | null;
|
||||
/**
|
||||
* 医院地址
|
||||
*/
|
||||
Address?: string | null;
|
||||
/**
|
||||
* 联系电话
|
||||
*/
|
||||
Contact?: string | null;
|
||||
/**
|
||||
* 位置信息
|
||||
*/
|
||||
LocationInfo?: { [key: string]: any };
|
||||
/**
|
||||
* 启用状态
|
||||
*/
|
||||
Enabled: number;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
CreateTime?: string | null;
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
UpdateTime?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员信息
|
||||
*/
|
||||
interface MemberEntity extends import('src/views/system/member/model')['MemberEntity'] {
|
||||
/**
|
||||
* 会员ID
|
||||
*/
|
||||
Id: string | null;
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
Mobile: string | null;
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
Name: string | null;
|
||||
/**
|
||||
* 性别
|
||||
*/
|
||||
Gender: '男' | '女' | null;
|
||||
/**
|
||||
* 出生年月日
|
||||
*/
|
||||
BirthDate: string | null;
|
||||
/**
|
||||
* 微信openid
|
||||
*/
|
||||
WeChatOpenId: string | null;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
Remark?: string | null;
|
||||
/**
|
||||
* 启用状态
|
||||
*/
|
||||
Enabled: number;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
CreateTime?: string | null;
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
UpdateTime?: string | null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Drawer, Form, FormItem, Textarea, Switch, Cascader } from '@/components';
|
||||
import { pageData, editorData } from './page';
|
||||
import { OPTIONS } from '@skyfox2000/webbase';
|
||||
import { locationTreeUrl } from '../../system/location/page';
|
||||
import DoctorSelect from '@/views/system/doctor/select.vue';
|
||||
|
||||
const formData = ref<SamplingHelperEntity>(editorData.formData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer title="取样帮手配置" :page-data="pageData">
|
||||
<Form>
|
||||
<FormItem label="所在地区" rule="LocationInfo.LocationIds">
|
||||
<Cascader
|
||||
v-model:value="formData.LocationInfo!.LocationIds"
|
||||
:url="locationTreeUrl"
|
||||
v-model:label-info="formData.LocationInfo!.LocationNames"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="医生" rule="DoctorId">
|
||||
<DoctorSelect v-model:value="formData.DoctorId" :autoload="true" />
|
||||
</FormItem>
|
||||
<FormItem label="取样流程" rule="Process">
|
||||
<Textarea v-model:value="formData.Process" :auto-size="{ minRows: 3, maxRows: 10 }" />
|
||||
</FormItem>
|
||||
<FormItem label="启用状态">
|
||||
<Switch v-model:checked="formData.Enabled" :data="OPTIONS.EnableDisable" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts" setup>
|
||||
import { Table } from '@/components';
|
||||
import { pageData, useGridInit } from './page';
|
||||
useGridInit();
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<Table :page-data="pageData" />
|
||||
</template>
|
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { Content } from '@/components';
|
||||
import Grid from './grid.vue';
|
||||
import Search from './search.vue';
|
||||
import { pageData, usePageInit } from './page';
|
||||
usePageInit();
|
||||
|
||||
// 定义异步组件
|
||||
const Editor = defineAsyncComponent(() => import('./editor.vue'));
|
||||
</script>
|
||||
<template>
|
||||
<Content>
|
||||
<Search />
|
||||
<Grid />
|
||||
</Content>
|
||||
<component :is="Editor" v-if="pageData.editor?.visible" />
|
||||
</template>
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* 取样帮手配置
|
||||
*/
|
||||
export interface SamplingHelperEntity {
|
||||
/**
|
||||
* 配置ID
|
||||
*/
|
||||
Id: string | null;
|
||||
/**
|
||||
* 医生
|
||||
*/
|
||||
DoctorId: string | null;
|
||||
/**
|
||||
* 地区信息
|
||||
*/
|
||||
LocationInfo?: { [key: string]: any };
|
||||
/**
|
||||
* 取样流程
|
||||
*/
|
||||
Process: string | null;
|
||||
/**
|
||||
* 启用状态
|
||||
*/
|
||||
Enabled: number;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
CreateTime?: string | null;
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
UpdateTime?: string | null;
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* 启动配置
|
||||
* API操作
|
||||
*/
|
||||
import { usePageFactory, ApiUrls, ValidateRule } from '@skyfox2000/webbase';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const SamplingHelperUrl: ApiUrls = {
|
||||
/**
|
||||
* api
|
||||
*/
|
||||
api: 'PLATFORM_API',
|
||||
authorize: true,
|
||||
urls: {
|
||||
/**
|
||||
* 查询地址
|
||||
*/
|
||||
list: {
|
||||
url: '/api/RCSamplingHelperSrv/list',
|
||||
},
|
||||
/**
|
||||
* 保存地址
|
||||
*/
|
||||
save: {
|
||||
url: '/api/RCSamplingHelperSrv/save',
|
||||
},
|
||||
/**
|
||||
* 删除地址
|
||||
*/
|
||||
delete: {
|
||||
url: '/api/RCSamplingHelperSrv/remove',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单默认数据
|
||||
*/
|
||||
const defaultData: SamplingHelperEntity = {
|
||||
Id: null,
|
||||
DoctorId: null,
|
||||
LocationInfo: {
|
||||
LocationIds: [],
|
||||
},
|
||||
Process: null,
|
||||
Enabled: 1,
|
||||
CreateTime: null,
|
||||
UpdateTime: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单验证条件
|
||||
* # 适当验证条件
|
||||
* # async-validator的语法规范
|
||||
*/
|
||||
const formRules: Record<string, ValidateRule> = {
|
||||
DoctorId: {
|
||||
required: true,
|
||||
message: '医生不能为空',
|
||||
},
|
||||
Process: {
|
||||
required: true,
|
||||
message: '取样流程不能为空',
|
||||
},
|
||||
};
|
||||
|
||||
export const { editorData, gridData, pageData, usePageInit, useEditorInit, useGridInit } =
|
||||
usePageFactory<SamplingHelperEntity>(SamplingHelperUrl, defaultData, formRules);
|
||||
|
||||
gridData.gridQuery = {
|
||||
Option: {},
|
||||
Query: {
|
||||
$order: [['UpdateTime', 'desc']], // # 合适的默认查询条件,比如更新时间
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
以下为数据列表头定义
|
||||
# 不需要显示CreateTime/UpdateTime
|
||||
*/
|
||||
const columns = ref([
|
||||
{
|
||||
title: '医生',
|
||||
dataIndex: 'DoctorName',
|
||||
width: 120,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '所在地区',
|
||||
key: 'LocationInfo.LocationNames',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
customRender: ({ record }: { record: HospitalEntity }) => {
|
||||
return record.LocationInfo?.LocationNames ? record.LocationInfo?.LocationNames.join('/') : '';
|
||||
},
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '取样流程',
|
||||
dataIndex: 'Process',
|
||||
width: 200,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'enabled',
|
||||
width: 80,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
width: 100,
|
||||
responsive: ['sm'],
|
||||
},
|
||||
]);
|
||||
gridData.columns = columns.value;
|
||||
gridData.tableSize = 'small';
|
||||
gridData.remotePage = false;
|
||||
gridData.tools = ['Reload', 'Fullscreen'];
|
|
@ -0,0 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Search, SearchItem, Input } from '@/components';
|
||||
import { pageData } from './page';
|
||||
|
||||
// #合适的1至2个查询条件,以及下面对应的修改,
|
||||
// #合适的组件,所有组件从@/components获取
|
||||
const searchData = ref<Record<string, any>>({
|
||||
DoctorName: undefined,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Search v-model:search="searchData" :page-data="pageData">
|
||||
<SearchItem label="医生名">
|
||||
<Input v-model:value="searchData.DoctorName" />
|
||||
</SearchItem>
|
||||
</Search>
|
||||
</template>
|
|
@ -0,0 +1,33 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Drawer, Form, FormItem, Input, Textarea } from '@/components';
|
||||
import { pageData, editorData } from './page';
|
||||
import Status from './status.vue';
|
||||
|
||||
const formData = ref<OrderEntity>(editorData.formData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer title="订单信息" :page-data="pageData">
|
||||
<Form>
|
||||
<FormItem label="会员名">
|
||||
<Input v-model:value="formData.MemberName" />
|
||||
</FormItem>
|
||||
<FormItem label="产品名称">
|
||||
<Input v-model:value="formData.ProductName" />
|
||||
</FormItem>
|
||||
<FormItem label="价码">
|
||||
<Input v-model:value="formData.PriceCode" />
|
||||
</FormItem>
|
||||
<FormItem label="快递号">
|
||||
<Input v-model:value="formData.ExpressNumber" />
|
||||
</FormItem>
|
||||
<FormItem label="订单状态" rule="Status">
|
||||
<Status v-model:value="formData.Status" />
|
||||
</FormItem>
|
||||
<FormItem label="备注">
|
||||
<Textarea v-model:value="formData.Remark" :auto-size="{ minRows: 3, maxRows: 6 }" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts" setup>
|
||||
import { Table } from '@/components';
|
||||
import { pageData, useGridInit } from './page';
|
||||
useGridInit();
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<Table :page-data="pageData" />
|
||||
</template>
|
|
@ -0,0 +1,26 @@
|
|||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { Content } from '@/components';
|
||||
import UploadForm from '@/components/content/dialog/uploadForm.vue';
|
||||
import Grid from './grid.vue';
|
||||
import Search from './search.vue';
|
||||
import { pageData, usePageInit } from './page';
|
||||
usePageInit();
|
||||
|
||||
// 定义异步组件
|
||||
const Editor = defineAsyncComponent(() => import('./editor.vue'));
|
||||
</script>
|
||||
<template>
|
||||
<Content>
|
||||
<Search />
|
||||
<Grid />
|
||||
</Content>
|
||||
<component :is="Editor" v-if="pageData.editor?.visible" />
|
||||
<UploadForm
|
||||
v-if="pageData.subEditor!.uploadForm.visible"
|
||||
width="600px"
|
||||
:upload-form="pageData.subEditor!.uploadForm"
|
||||
:max-count="1"
|
||||
:page-data="pageData"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* 订单信息
|
||||
*/
|
||||
export interface OrderEntity {
|
||||
/**
|
||||
* 订单ID
|
||||
*/
|
||||
Id: string | null;
|
||||
/**
|
||||
* 所属会员
|
||||
*/
|
||||
MemberId: string | null;
|
||||
/**
|
||||
* 所属会员
|
||||
*/
|
||||
MemberName: string | null;
|
||||
/**
|
||||
* 价码ID
|
||||
*/
|
||||
PriceId: string | null;
|
||||
/**
|
||||
* 价码
|
||||
*/
|
||||
PriceCode: string | null;
|
||||
/**
|
||||
* 产品名称
|
||||
*/
|
||||
ProductName: string | null;
|
||||
/**
|
||||
* 会员地址
|
||||
*/
|
||||
Address: string | null;
|
||||
/**
|
||||
* 订单快递号
|
||||
*/
|
||||
ExpressNumber?: string | null;
|
||||
/**
|
||||
* 快递地址信息
|
||||
*/
|
||||
ExpressInfo?: { [key: string]: any };
|
||||
/**
|
||||
* 检查报告
|
||||
*/
|
||||
ReportFileUrl?: string | null;
|
||||
/**
|
||||
* 订单状态
|
||||
*/
|
||||
Status: string | null;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
Remark?: string | null;
|
||||
/**
|
||||
* 启用状态
|
||||
*/
|
||||
Enabled: number;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
CreateTime?: string | null;
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
UpdateTime?: string | null;
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
/**
|
||||
* 启动配置
|
||||
* API操作
|
||||
*/
|
||||
import { usePageFactory, ApiUrls, ValidateRule, ButtonTool, exportSelectedRows, EditorData } from '@skyfox2000/webbase';
|
||||
import { ref } from 'vue';
|
||||
import message from 'vue-m-message';
|
||||
|
||||
export const OrderUrl: ApiUrls = {
|
||||
/**
|
||||
* api
|
||||
*/
|
||||
api: 'PLATFORM_API',
|
||||
authorize: true,
|
||||
urls: {
|
||||
/**
|
||||
* 查询地址
|
||||
*/
|
||||
list: {
|
||||
url: '/api/RCOrderSrv/list',
|
||||
},
|
||||
/**
|
||||
* 保存地址
|
||||
*/
|
||||
save: {
|
||||
url: '/api/RCOrderSrv/save',
|
||||
},
|
||||
/**
|
||||
* 删除地址
|
||||
*/
|
||||
delete: {
|
||||
url: '/api/RCOrderSrv/remove',
|
||||
},
|
||||
|
||||
upload: {
|
||||
url: '',
|
||||
header: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
authorize: true, // 需要授权
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单默认数据
|
||||
*/
|
||||
const defaultData: OrderEntity = {
|
||||
Id: null,
|
||||
MemberId: null,
|
||||
MemberName: null,
|
||||
PriceId: null,
|
||||
PriceCode: null,
|
||||
ProductName: null,
|
||||
Address: null,
|
||||
ExpressNumber: null,
|
||||
ExpressInfo: {},
|
||||
ReportFileUrl: null,
|
||||
Status: null,
|
||||
Remark: null,
|
||||
Enabled: 1,
|
||||
CreateTime: null,
|
||||
UpdateTime: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单验证条件
|
||||
* # 适当验证条件
|
||||
* # async-validator的语法规范
|
||||
*/
|
||||
const formRules: Record<string, ValidateRule> = {
|
||||
Status: {
|
||||
required: true,
|
||||
message: '订单状态不能为空',
|
||||
},
|
||||
};
|
||||
|
||||
export const { editorData, gridData, pageData, usePageInit, useEditorInit, useGridInit } = usePageFactory<OrderEntity>(
|
||||
OrderUrl,
|
||||
defaultData,
|
||||
formRules,
|
||||
);
|
||||
|
||||
editorData.saveAsBtnVisible = false;
|
||||
|
||||
gridData.gridQuery = {
|
||||
Option: {},
|
||||
Query: {
|
||||
$order: [['UpdateTime', 'desc']], // # 合适的默认查询条件,比如更新时间
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
以下为数据列表头定义
|
||||
# 不需要显示CreateTime/UpdateTime
|
||||
*/
|
||||
const columns = ref([
|
||||
{
|
||||
title: '订单号',
|
||||
dataIndex: 'OrderNo',
|
||||
width: 120,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '会员名',
|
||||
dataIndex: 'MemberName',
|
||||
width: 120,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'Mobile',
|
||||
width: 120,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '价码',
|
||||
dataIndex: 'PriceCode',
|
||||
width: 100,
|
||||
responsive: ['lg'],
|
||||
},
|
||||
{
|
||||
title: '订单快递号',
|
||||
dataIndex: 'ExpressNumber',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
responsive: ['lg'],
|
||||
},
|
||||
{
|
||||
title: '订单状态',
|
||||
dataIndex: 'Status',
|
||||
width: 100,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
width: 150,
|
||||
export: false,
|
||||
responsive: ['sm'],
|
||||
},
|
||||
]);
|
||||
gridData.columns = columns.value;
|
||||
gridData.tableSize = 'small';
|
||||
gridData.remotePage = false;
|
||||
gridData.selectKeys = [];
|
||||
gridData.selectRows = [];
|
||||
gridData.selectable = true;
|
||||
let download: ButtonTool = {
|
||||
label: '下载',
|
||||
key: 'download',
|
||||
type: 'primary',
|
||||
icon: 'icon-download',
|
||||
click: () => {
|
||||
if (gridData.selectRows && gridData.selectRows.length > 0) {
|
||||
exportSelectedRows<OrderEntity>('订单-{YYYY-MM-DD}.csv', columns.value, gridData.selectRows);
|
||||
} else {
|
||||
message.warning('请选择需要下载的订单!');
|
||||
}
|
||||
},
|
||||
};
|
||||
gridData.buttons = ['New', download];
|
||||
|
||||
// 文件上传弹窗
|
||||
let uploadForm: EditorData<OrderEntity> = {
|
||||
visible: false,
|
||||
formData: JSON.parse(JSON.stringify(defaultData)),
|
||||
isFormSaving: false,
|
||||
isFormLoading: false,
|
||||
};
|
||||
pageData.value.subEditor = {
|
||||
uploadForm,
|
||||
};
|
||||
|
||||
gridData.tools = ['Reload', 'Fullscreen'];
|
||||
let upload: ButtonTool = {
|
||||
label: '上传报告',
|
||||
key: 'upload',
|
||||
type: 'default',
|
||||
icon: 'icon-upload',
|
||||
click: (_, row) => {
|
||||
pageData.value.subEditor!.uploadForm.formData = row;
|
||||
pageData.value.subEditor!.uploadForm.visible = !pageData.value.subEditor!.uploadForm.visible;
|
||||
},
|
||||
};
|
||||
gridData.operates = [upload, 'Edit', 'Delete'];
|
|
@ -0,0 +1,36 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Search, SearchItem, Input } from '@/components';
|
||||
import { pageData } from './page';
|
||||
import Status from './status.vue';
|
||||
|
||||
// #合适的1至2个查询条件,以及下面对应的修改,
|
||||
// #合适的组件,所有组件从@/components获取
|
||||
const searchData = ref<Record<string, any>>({
|
||||
OrderNo: null,
|
||||
Mobile: null,
|
||||
MemberName: null,
|
||||
ProductName: null,
|
||||
Status: null,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Search v-model:search="searchData" :page-data="pageData">
|
||||
<SearchItem label="订单号">
|
||||
<Input v-model:value="searchData.OrderNo" />
|
||||
</SearchItem>
|
||||
<SearchItem label="手机号">
|
||||
<Input v-model:value="searchData.Mobile" />
|
||||
</SearchItem>
|
||||
<SearchItem label="会员名">
|
||||
<Input v-model:value="searchData.MemberName" />
|
||||
</SearchItem>
|
||||
<SearchItem label="产品名称">
|
||||
<Input v-model:value="searchData.ProductName" />
|
||||
</SearchItem>
|
||||
<SearchItem label="订单状态">
|
||||
<Status :all="true" v-model:value="searchData.Status" />
|
||||
</SearchItem>
|
||||
</Search>
|
||||
</template>
|
|
@ -0,0 +1,44 @@
|
|||
<script setup lang="ts">
|
||||
import { Select } from '@/components';
|
||||
const props = defineProps({
|
||||
all: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const options: Array<{
|
||||
label: string;
|
||||
value: string | null;
|
||||
}> = [
|
||||
{
|
||||
label: '待付款',
|
||||
value: '待付款',
|
||||
},
|
||||
{
|
||||
label: '待发货',
|
||||
value: '待发货',
|
||||
},
|
||||
{
|
||||
label: '待收货',
|
||||
value: '待收货',
|
||||
},
|
||||
{
|
||||
label: '已完成',
|
||||
value: '已完成',
|
||||
},
|
||||
{
|
||||
label: '已取消',
|
||||
value: '已取消',
|
||||
},
|
||||
];
|
||||
if (props.all) {
|
||||
options.unshift({
|
||||
label: '全部',
|
||||
value: null,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select :data="options"></Select>
|
||||
</template>
|
|
@ -0,0 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Drawer, Form, FormItem, Input, Textarea, InputNumber, Switch } from '@/components';
|
||||
import { pageData, editorData } from './page';
|
||||
import { OPTIONS } from '@skyfox2000/webbase';
|
||||
|
||||
const formData = ref<PriceQREntity>(editorData.formData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer title="价码信息" :page-data="pageData">
|
||||
<Form>
|
||||
<FormItem label="名称" rule="Name">
|
||||
<Input v-model:value="formData.Name" />
|
||||
</FormItem>
|
||||
<FormItem label="价格码" rule="Code">
|
||||
<Input v-model:value="formData.Code" />
|
||||
</FormItem>
|
||||
<FormItem label="单价" rule="Price">
|
||||
<InputNumber v-model:value="formData.Price" />
|
||||
</FormItem>
|
||||
<FormItem label="备注">
|
||||
<Textarea v-model:value="formData.Remark" :auto-size="{ minRows: 2, maxRows: 5 }" />
|
||||
</FormItem>
|
||||
<FormItem label="启用状态">
|
||||
<Switch v-model:checked="formData.Enabled" :data="OPTIONS.EnableDisable" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts" setup>
|
||||
import { Table } from '@/components';
|
||||
import { pageData, useGridInit } from './page';
|
||||
useGridInit();
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<Table :page-data="pageData" />
|
||||
</template>
|
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { Content } from '@/components';
|
||||
import Grid from './grid.vue';
|
||||
import Search from './search.vue';
|
||||
import { pageData, usePageInit } from './page';
|
||||
usePageInit();
|
||||
|
||||
// 定义异步组件
|
||||
const Editor = defineAsyncComponent(() => import('./editor.vue'));
|
||||
</script>
|
||||
<template>
|
||||
<Content>
|
||||
<Search />
|
||||
<Grid />
|
||||
</Content>
|
||||
<component :is="Editor" v-if="pageData.editor?.visible" />
|
||||
</template>
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* 价码信息
|
||||
*/
|
||||
export interface PriceQREntity {
|
||||
/**
|
||||
* 价格ID
|
||||
*/
|
||||
Id: string | null;
|
||||
/**
|
||||
* 价格码
|
||||
*/
|
||||
Code: string | null;
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
Name: string | null;
|
||||
/**
|
||||
* 单价
|
||||
*/
|
||||
Price: number | null;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
Remark?: string | null;
|
||||
/**
|
||||
* 启用状态
|
||||
*/
|
||||
Enabled: number;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
CreateTime?: string | null;
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
UpdateTime?: string | null;
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
/**
|
||||
* 启动配置
|
||||
* API操作
|
||||
*/
|
||||
import { usePageFactory, ApiUrls, ValidateRule } from '@skyfox2000/webbase';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const PriceQRUrl: ApiUrls = {
|
||||
/**
|
||||
* api
|
||||
*/
|
||||
api: 'PLATFORM_API',
|
||||
authorize: true,
|
||||
urls: {
|
||||
/**
|
||||
* 查询地址
|
||||
*/
|
||||
find: {
|
||||
url: '/api/RCPriceQRSrv/find',
|
||||
},
|
||||
/**
|
||||
* 保存地址
|
||||
*/
|
||||
save: {
|
||||
url: '/api/RCPriceQRSrv/save',
|
||||
},
|
||||
/**
|
||||
* 删除地址
|
||||
*/
|
||||
delete: {
|
||||
url: '/api/RCPriceQRSrv/remove',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单默认数据
|
||||
*/
|
||||
const defaultData: PriceQREntity = {
|
||||
Id: null,
|
||||
Code: null,
|
||||
Name: null,
|
||||
Price: null,
|
||||
Remark: null,
|
||||
Enabled: 1,
|
||||
CreateTime: null,
|
||||
UpdateTime: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单验证条件
|
||||
* # 适当验证条件
|
||||
* # async-validator的语法规范
|
||||
*/
|
||||
const formRules: Record<string, ValidateRule> = {
|
||||
Code: {
|
||||
required: true,
|
||||
message: '价格码不能为空',
|
||||
},
|
||||
Name: {
|
||||
required: true,
|
||||
message: '名称不能为空',
|
||||
},
|
||||
Price: {
|
||||
required: true,
|
||||
message: '单价不能为空',
|
||||
},
|
||||
};
|
||||
|
||||
export const { editorData, gridData, pageData, usePageInit, useEditorInit, useGridInit } =
|
||||
usePageFactory<PriceQREntity>(PriceQRUrl, defaultData, formRules);
|
||||
|
||||
gridData.gridQuery = {
|
||||
Option: {},
|
||||
Query: {
|
||||
$order: [['UpdateTime', 'desc']], // # 合适的默认查询条件,比如更新时间
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
以下为数据列表头定义
|
||||
# 不需要显示CreateTime/UpdateTime
|
||||
*/
|
||||
const columns = ref([
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'Name',
|
||||
width: 150,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '价格码',
|
||||
dataIndex: 'Code',
|
||||
width: 120,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '单价',
|
||||
dataIndex: 'Price',
|
||||
width: 100,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'Remark',
|
||||
width: 150,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'enabled',
|
||||
width: 80,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
width: 100,
|
||||
responsive: ['sm'],
|
||||
},
|
||||
]);
|
||||
gridData.columns = columns.value;
|
||||
gridData.tableSize = 'small';
|
||||
gridData.tools = ['Reload', 'Fullscreen'];
|
|
@ -0,0 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { Search, SearchItem, Input } from "@/components";
|
||||
import { pageData } from "./page";
|
||||
|
||||
// #合适的1至2个查询条件,以及下面对应的修改,
|
||||
// #合适的组件,所有组件从@/components获取
|
||||
const searchData = ref<Record<string, any>>({
|
||||
Code: undefined,
|
||||
Name: undefined,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Search v-model:search="searchData" :page-data="pageData">
|
||||
<SearchItem label="价格码">
|
||||
<Input v-model:value="searchData.Code" />
|
||||
</SearchItem>
|
||||
<SearchItem label="名称">
|
||||
<Input v-model:value="searchData.Name" />
|
||||
</SearchItem>
|
||||
</Search>
|
||||
</template>
|
|
@ -0,0 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import LoginBox from './login.vue';
|
||||
import { HostInfo } from '@skyfox2000/microbase';
|
||||
import { getHostInfo } from '@skyfox2000/webbase';
|
||||
const hostInfo = ref<HostInfo>({
|
||||
Host: '',
|
||||
Title: '管理系统',
|
||||
FullTitle: '管理系统',
|
||||
Provider: 'XXXX有限公司',
|
||||
});
|
||||
|
||||
hostInfo.value = getHostInfo();
|
||||
|
||||
document.title = '用户登录-' + hostInfo.value.Title;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white text-[#424242]">
|
||||
<!-- Header -->
|
||||
<header
|
||||
class="w-full h-[100px] bg-gradient-to-r from-[#618dde] to-[#e3f1ff] flex justify-start items-center pl-[50px]"
|
||||
>
|
||||
<div class="text-[28px] text-white">{{ hostInfo.Title }}</div>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<main
|
||||
class="w-full bg-[#e3f1ff] border-t-4 border-b-4 border-[#769BDA] flex justify-end items-center min-h-[calc(100vh-200px)] bg-no-repeat bg-[url('@/assets/images/login/banner.png')] bg-[25%_center] bg-[length:600px_auto]"
|
||||
>
|
||||
<div class="w-full pr-[9%]">
|
||||
<div class="mx-auto max-w-[1280px] flex justify-end">
|
||||
<LoginBox :title="hostInfo.Title" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-[#f5f5f5]/90 w-full min-h-[100px] flex justify-start items-center">
|
||||
<div class="w-full flex justify-around items-center">
|
||||
<div class="w-1/3"></div>
|
||||
<div class="w-1/3 text-center text-gray-500">
|
||||
© 2023 {{ hostInfo.FullTitle }} - {{ hostInfo.Provider }} 提供技术支持
|
||||
</div>
|
||||
<div class="w-1/3"></div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,151 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import Message from 'vue-m-message';
|
||||
import { Input, InputPassword } from '@/components';
|
||||
import { userLogin } from '@skyfox2000/webbase';
|
||||
|
||||
defineProps({
|
||||
title: String,
|
||||
});
|
||||
|
||||
const onLoginClicked = ref(userLogin);
|
||||
|
||||
const name = ref('');
|
||||
const password = ref('');
|
||||
|
||||
const nameTip = ref('请输入用户名');
|
||||
const nameStatus = ref<'' | 'error' | 'warning' | undefined>('');
|
||||
const passwordTip = ref('请输入密码');
|
||||
const passwordStatus = ref<'' | 'error' | 'warning' | undefined>('');
|
||||
|
||||
watch(name, () => {
|
||||
nameStatus.value = '';
|
||||
nameTip.value = '请输入用户名';
|
||||
});
|
||||
watch(password, () => {
|
||||
passwordStatus.value = '';
|
||||
passwordTip.value = '请输入密码';
|
||||
});
|
||||
|
||||
const logining = ref<boolean>(false);
|
||||
const onClicked = async () => {
|
||||
nameTip.value = '请输入用户名';
|
||||
nameStatus.value = '';
|
||||
passwordTip.value = '请输入密码';
|
||||
passwordStatus.value = '';
|
||||
if (name.value === '') {
|
||||
nameTip.value = '用户名不能为空';
|
||||
nameStatus.value = 'error';
|
||||
}
|
||||
if (password.value === '') {
|
||||
passwordTip.value = '密码不能为空';
|
||||
passwordStatus.value = 'error';
|
||||
}
|
||||
|
||||
if (name.value !== '' && password.value !== '') {
|
||||
if (onLoginClicked.value) {
|
||||
logining.value = true;
|
||||
const result = await onLoginClicked.value({
|
||||
UserName: name.value,
|
||||
UserPass: password.value,
|
||||
});
|
||||
setTimeout(() => {
|
||||
logining.value = false;
|
||||
}, 1500);
|
||||
|
||||
if (result && result.errno) {
|
||||
nameTip.value = result.msg!;
|
||||
nameStatus.value = 'error';
|
||||
passwordTip.value = result.msg!;
|
||||
passwordStatus.value = 'error';
|
||||
}
|
||||
} else {
|
||||
Message.error('未配置登录接口!');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="w-[300px] h-[343px] flex mt-[2px] border-[3px] border-blue-300 rounded-[10px] shadow-lg flex-col items-center bg-white px-[16px]"
|
||||
>
|
||||
<div class="w-[84%]">
|
||||
<div class="text-center text-gray-500 font-medium mt-[30px] mb-[22px]">{{ title }} —— 用户登录</div>
|
||||
</div>
|
||||
<div class="w-[84%] h-[74px]">
|
||||
<Input placeholder="请输入用户名" v-model:value="name" :status="nameStatus" :maxlength="30">
|
||||
<template #prefix>
|
||||
<i class="iconfont icon-user text-gray-400" :class="[nameStatus === 'error' ? 'text-red-400' : '']"></i>
|
||||
</template>
|
||||
</Input>
|
||||
<span class="mt-1 hidden" :class="[nameStatus]">{{ nameTip }}</span>
|
||||
</div>
|
||||
<div class="w-[84%] h-[70px]">
|
||||
<InputPassword placeholder="请输入密码" v-model:value="password" :status="passwordStatus" :maxlength="30">
|
||||
<template #prefix>
|
||||
<i
|
||||
class="iconfont icon-mima text-gray-400"
|
||||
:class="[passwordStatus === 'error' ? 'text-red-400' : '']"
|
||||
></i>
|
||||
</template>
|
||||
</InputPassword>
|
||||
<span class="mt-1 hidden" :class="[passwordStatus]">{{ passwordTip }}</span>
|
||||
</div>
|
||||
<div class="w-[84%] h-[34px]">
|
||||
<!-- <Col>忘记密码?</Col> -->
|
||||
</div>
|
||||
<div class="w-[84%]">
|
||||
<button
|
||||
:class="[
|
||||
'w-full',
|
||||
'border-[2px]',
|
||||
'border-solid',
|
||||
'border-[#90B0E3] hover:border-[#769BDA]',
|
||||
'bg-[#769BDA]' /* 替换为接近 #4476D3 的 Tailwind 颜色 */,
|
||||
'focus:ring-4',
|
||||
'focus:ring-blue-300',
|
||||
'text-base' /* 调整文字大小 */,
|
||||
'font-normal' /* 调整文字粗细为正常 */,
|
||||
'text-white',
|
||||
'text-center',
|
||||
'py-2',
|
||||
'px-4',
|
||||
'rounded-xl' /* 增加圆角 */,
|
||||
'disabled:bg-gray-300',
|
||||
'disabled:cursor-not-allowed',
|
||||
logining ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-[#90B0E3]',
|
||||
]"
|
||||
@click="onClicked"
|
||||
:disabled="logining"
|
||||
>
|
||||
<i class="anticon-spin iconfont icon-loading loading" v-if="logining"></i>
|
||||
<span class="ml-[10px]">登 录</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="less">
|
||||
.loading {
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
|
||||
animation: loading 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
display: block;
|
||||
color: #ff0000;
|
||||
font-size: 12px;
|
||||
margin-left: 14px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Drawer, Form, FormItem, Input, Textarea, InputPassword, Switch } from '@/components';
|
||||
import { pageData, editorData } from './page';
|
||||
import { OPTIONS } from '@skyfox2000/webbase';
|
||||
|
||||
const formData = ref<AccountEntity>(editorData.formData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer title="账号信息" :page-data="pageData">
|
||||
<Form>
|
||||
<FormItem label="登录账号" rule="UserCode">
|
||||
<Input v-model:value="formData.UserCode" />
|
||||
</FormItem>
|
||||
<FormItem label="姓名" rule="Name">
|
||||
<Input v-model:value="formData.Name" />
|
||||
</FormItem>
|
||||
<FormItem label="密码" rule="UserPwd">
|
||||
<InputPassword v-model:value="formData.UserPwd" />
|
||||
</FormItem>
|
||||
<FormItem label="备注">
|
||||
<Textarea v-model:value="formData.Remark" :auto-size="{ minRows: 2, maxRows: 5 }" />
|
||||
</FormItem>
|
||||
<FormItem label="启用状态">
|
||||
<Switch v-model:checked="formData.Enabled" :data="OPTIONS.EnableDisable" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts" setup>
|
||||
import { Table } from '@/components';
|
||||
import { pageData, useGridInit } from './page';
|
||||
useGridInit();
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<Table :page-data="pageData" />
|
||||
</template>
|
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { Content } from '@/components';
|
||||
import Grid from './grid.vue';
|
||||
import Search from './search.vue';
|
||||
import { pageData, usePageInit } from './page';
|
||||
usePageInit();
|
||||
|
||||
// 定义异步组件
|
||||
const Editor = defineAsyncComponent(() => import('./editor.vue'));
|
||||
</script>
|
||||
<template>
|
||||
<Content>
|
||||
<Search />
|
||||
<Grid />
|
||||
</Content>
|
||||
<component :is="Editor" v-if="pageData.editor?.visible" />
|
||||
</template>
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* 账号
|
||||
*/
|
||||
export interface AccountEntity {
|
||||
/**
|
||||
* 帐号类型(Admin管理员/User操作员)
|
||||
*/
|
||||
AccountType: string | null;
|
||||
/**
|
||||
* 所属租户
|
||||
*/
|
||||
ClientId?: string | null;
|
||||
/**
|
||||
* 主部门
|
||||
*/
|
||||
DepartId?: string | null;
|
||||
/**
|
||||
* 启用状态
|
||||
*/
|
||||
Enabled: number;
|
||||
/**
|
||||
* 主键Id
|
||||
*/
|
||||
Id: string | null;
|
||||
/**
|
||||
* 主职位
|
||||
*/
|
||||
JobTitleId?: string | null;
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
Name: string | null;
|
||||
/**
|
||||
* 密码时间
|
||||
*/
|
||||
PassTime?: string | null;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
Remark?: string | null;
|
||||
/**
|
||||
* 审核状态
|
||||
*/
|
||||
State?: string | null;
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
Tags?: string[];
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
UpdateTime?: string | null;
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
UpdateUser?: string | null;
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
UserCode: string | null;
|
||||
/**
|
||||
* 用户级别(Platform平台/Client租户/Member会员)
|
||||
*/
|
||||
UserLevel: string | null;
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
UserProps?: { [key: string]: any };
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
UserPwd?: string | null;
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* 启动配置
|
||||
* API操作
|
||||
*/
|
||||
import { usePageFactory, ApiUrls, ValidateRule } from '@skyfox2000/webbase';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const AccountUrl: ApiUrls = {
|
||||
/**
|
||||
* api
|
||||
*/
|
||||
api: 'PLATFORM_API',
|
||||
authorize: true,
|
||||
urls: {
|
||||
/**
|
||||
* 查询地址
|
||||
*/
|
||||
find: {
|
||||
url: '/api/RCAccountOpSrv/find',
|
||||
},
|
||||
/**
|
||||
* 保存地址
|
||||
*/
|
||||
save: {
|
||||
url: '/api/RCAccountOpSrv/save',
|
||||
},
|
||||
/**
|
||||
* 删除地址
|
||||
*/
|
||||
delete: {
|
||||
url: '/api/RCAccountOpSrv/remove',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单默认数据
|
||||
*/
|
||||
const defaultData: AccountEntity = {
|
||||
Id: null,
|
||||
ClientId: null,
|
||||
UserCode: null,
|
||||
UserPwd: null,
|
||||
Name: null,
|
||||
AccountType: 'Admin',
|
||||
UserLevel: 'Platform',
|
||||
JobTitleId: null,
|
||||
DepartId: null,
|
||||
Tags: [],
|
||||
UserProps: {},
|
||||
PassTime: null,
|
||||
Remark: null,
|
||||
State: null,
|
||||
Enabled: 1,
|
||||
UpdateTime: null,
|
||||
UpdateUser: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单验证条件
|
||||
* # 适当验证条件
|
||||
* # async-validator的语法规范
|
||||
*/
|
||||
const formRules: Record<string, ValidateRule> = {
|
||||
UserCode: {
|
||||
required: true,
|
||||
message: '登录账号不能为空',
|
||||
},
|
||||
Name: {
|
||||
required: true,
|
||||
message: '姓名不能为空',
|
||||
},
|
||||
UserPwd: {
|
||||
required: true,
|
||||
message: '密码不能为空',
|
||||
},
|
||||
};
|
||||
|
||||
export const { editorData, gridData, pageData, usePageInit, useEditorInit, useGridInit } =
|
||||
usePageFactory<AccountEntity>(AccountUrl, defaultData, formRules);
|
||||
|
||||
gridData.gridQuery = {
|
||||
Option: {},
|
||||
Query: {
|
||||
$order: [['UpdateTime', 'desc']], // # 合适的默认查询条件,比如更新时间
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
以下为数据列表头定义
|
||||
# 不需要显示CreateTime/UpdateTime
|
||||
*/
|
||||
const columns = ref([
|
||||
{
|
||||
title: '登录账号',
|
||||
dataIndex: 'UserCode',
|
||||
width: 100,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'Name',
|
||||
width: 100,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'Remark',
|
||||
width: 100,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'enabled',
|
||||
width: 80,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
width: 100,
|
||||
responsive: ['sm'],
|
||||
},
|
||||
]);
|
||||
gridData.columns = columns.value;
|
||||
gridData.tableSize = 'small';
|
||||
gridData.tools = ['Reload', 'Fullscreen'];
|
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { Search, SearchItem, Input } from "@/components";
|
||||
import { pageData } from "./page";
|
||||
|
||||
const searchData = ref<Record<string, any>>({
|
||||
UserCode: undefined,
|
||||
Name: undefined,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Search v-model:search="searchData" :page-data="pageData">
|
||||
<SearchItem label="登录账号">
|
||||
<Input v-model:value="searchData.UserCode" />
|
||||
</SearchItem>
|
||||
<SearchItem label="姓名">
|
||||
<Input v-model:value="searchData.Name" />
|
||||
</SearchItem>
|
||||
</Search>
|
||||
</template>
|
|
@ -0,0 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import { AutoComplete } from '@/components';
|
||||
import { DoctorUrl } from './page';
|
||||
import { ReqParams } from '@skyfox2000/fapi';
|
||||
|
||||
const onSearch = (search_value: String, query: ReqParams) => {
|
||||
query.Query = {
|
||||
Name: '%' + search_value + '%',
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AutoComplete
|
||||
:onsearch="onSearch"
|
||||
:url="DoctorUrl.urls.select"
|
||||
:params="{
|
||||
Option: {
|
||||
SelectFields: ['S_Doctor.Id', 'S_Doctor.Name'],
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,28 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Drawer, Form, FormItem, Input, Switch } from '@/components';
|
||||
import { pageData, editorData } from './page';
|
||||
import { OPTIONS } from '@skyfox2000/webbase';
|
||||
import HospitalSelect from '@/views/system/hospital/select.vue';
|
||||
|
||||
const formData = ref<DoctorEntity>(editorData.formData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer title="医生信息" :page-data="pageData">
|
||||
<Form>
|
||||
<FormItem label="姓名" rule="Name">
|
||||
<Input v-model:value="formData.Name" />
|
||||
</FormItem>
|
||||
<FormItem label="手机号" rule="Mobile">
|
||||
<Input v-model:value="formData.Mobile" />
|
||||
</FormItem>
|
||||
<FormItem label="所属医院" rule="HospitalId">
|
||||
<HospitalSelect v-model:value="formData.HospitalId" />
|
||||
</FormItem>
|
||||
<FormItem label="启用状态">
|
||||
<Switch v-model:checked="formData.Enabled" :data="OPTIONS.EnableDisable" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts" setup>
|
||||
import { Table } from '@/components';
|
||||
import { pageData, useGridInit } from './page';
|
||||
useGridInit();
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<Table :page-data="pageData" />
|
||||
</template>
|
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { Content } from '@/components';
|
||||
import Grid from './grid.vue';
|
||||
import Search from './search.vue';
|
||||
import { pageData, usePageInit } from './page';
|
||||
usePageInit();
|
||||
|
||||
// 定义异步组件
|
||||
const Editor = defineAsyncComponent(() => import('./editor.vue'));
|
||||
</script>
|
||||
<template>
|
||||
<Content>
|
||||
<Search />
|
||||
<Grid />
|
||||
</Content>
|
||||
<component :is="Editor" v-if="pageData.editor?.visible" />
|
||||
</template>
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* 医生账号
|
||||
*/
|
||||
export interface DoctorEntity {
|
||||
/**
|
||||
* 医生ID
|
||||
*/
|
||||
Id: string | null;
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
Mobile: string | null;
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
Name: string | null;
|
||||
/**
|
||||
* 所属医院
|
||||
*/
|
||||
HospitalId: string | null;
|
||||
/**
|
||||
* 启用状态
|
||||
*/
|
||||
Enabled: number;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
CreateTime?: string | null;
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
UpdateTime?: string | null;
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* 启动配置
|
||||
* API操作
|
||||
*/
|
||||
import { usePageFactory, ApiUrls, ValidateRule } from '@skyfox2000/webbase';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const DoctorUrl: ApiUrls = {
|
||||
/**
|
||||
* api
|
||||
*/
|
||||
api: 'PLATFORM_API',
|
||||
authorize: true,
|
||||
urls: {
|
||||
/**
|
||||
* 查询地址
|
||||
*/
|
||||
list: {
|
||||
url: '/api/RCDoctorSrv/list',
|
||||
},
|
||||
select: {
|
||||
url: '/api/RCDoctorSrv/select',
|
||||
cacheTime: 600, // 缓存600秒
|
||||
},
|
||||
/**
|
||||
* 保存地址
|
||||
*/
|
||||
save: {
|
||||
url: '/api/RCDoctorSrv/save',
|
||||
},
|
||||
/**
|
||||
* 删除地址
|
||||
*/
|
||||
delete: {
|
||||
url: '/api/RCDoctorSrv/remove',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单默认数据
|
||||
*/
|
||||
const defaultData: DoctorEntity = {
|
||||
Id: null,
|
||||
Mobile: null,
|
||||
Name: null,
|
||||
HospitalId: null,
|
||||
Enabled: 1,
|
||||
CreateTime: null,
|
||||
UpdateTime: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单验证条件
|
||||
* # 适当验证条件
|
||||
* # async-validator的语法规范
|
||||
*/
|
||||
const formRules: Record<string, ValidateRule> = {
|
||||
Mobile: {
|
||||
required: true,
|
||||
message: '手机号不能为空',
|
||||
},
|
||||
Name: {
|
||||
required: true,
|
||||
message: '姓名不能为空',
|
||||
},
|
||||
HospitalId: {
|
||||
required: true,
|
||||
message: '所属医院不能为空',
|
||||
},
|
||||
};
|
||||
|
||||
export const { editorData, gridData, pageData, usePageInit, useEditorInit, useGridInit } = usePageFactory<DoctorEntity>(
|
||||
DoctorUrl,
|
||||
defaultData,
|
||||
formRules,
|
||||
);
|
||||
|
||||
gridData.gridQuery = {
|
||||
Option: {},
|
||||
Query: {
|
||||
$order: [['UpdateTime', 'desc']], // # 合适的默认查询条件,比如更新时间
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
以下为数据列表头定义
|
||||
# 不需要显示CreateTime/UpdateTime
|
||||
*/
|
||||
const columns = ref([
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'Name',
|
||||
width: 100,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'Mobile',
|
||||
width: 120,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '所属医院',
|
||||
dataIndex: 'HospitalName',
|
||||
width: 150,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'enabled',
|
||||
width: 80,
|
||||
responsive: ['md'],
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
width: 100,
|
||||
responsive: ['sm'],
|
||||
},
|
||||
]);
|
||||
gridData.columns = columns.value;
|
||||
gridData.tableSize = 'small';
|
||||
gridData.remotePage = false;
|
||||
gridData.tools = ['Reload', 'Fullscreen'];
|
|
@ -0,0 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Search, SearchItem, Input } from '@/components';
|
||||
import { pageData } from './page';
|
||||
|
||||
// #合适的1至2个查询条件,以及下面对应的修改,
|
||||
// #合适的组件,所有组件从@/components获取
|
||||
const searchData = ref<Record<string, any>>({
|
||||
Mobile: undefined,
|
||||
Name: undefined,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Search v-model:search="searchData" :page-data="pageData">
|
||||
<SearchItem label="手机号">
|
||||
<Input v-model:value="searchData.Mobile" />
|
||||
</SearchItem>
|
||||
<SearchItem label="姓名">
|
||||
<Input v-model:value="searchData.Name" />
|
||||
</SearchItem>
|
||||
</Search>
|
||||
</template>
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import { Select } from '@/components';
|
||||
import { DoctorUrl } from './page';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select
|
||||
:url="DoctorUrl.urls.list"
|
||||
:params="{
|
||||
Option: {
|
||||
SelectFields: ['Id', 'Name'],
|
||||
},
|
||||
}"
|
||||
></Select>
|
||||
</template>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue