init: 橘视厅酒店版会员系统前端
19
.babelrc
Executable file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"presets": [
|
||||
["env", {
|
||||
"modules": false
|
||||
}],
|
||||
"stage-2"
|
||||
],
|
||||
"plugins": ["transform-runtime", ["component", [
|
||||
{
|
||||
"libraryName": "element-ui"
|
||||
}
|
||||
]]],
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": ["env", "stage-2"],
|
||||
"plugins": ["transform-es2015-modules-commonjs", "dynamic-import-node"]
|
||||
}
|
||||
}
|
||||
}
|
||||
27
.cursor/rules/camelcase-identifiers.mdc
Executable file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
description: 使用 AI 编写/修改代码时,标识符命名统一为小驼峰(camelCase)。
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
在本仓库中由 AI 生成或修改的代码须遵守以下命名约定:
|
||||
|
||||
## 小驼峰(camelCase)— 必须使用
|
||||
|
||||
- **变量**(`let` / `const` / `ref` / `reactive` 字段等)
|
||||
- **函数**、**方法**
|
||||
- **普通对象属性**(TypeScript 接口/类型中的字段名、请求体与前端模型字段,优先 camelCase)
|
||||
- **函数参数**与**回调形参**
|
||||
|
||||
禁止使用蛇形命名(`snake_case`)作为上述标识符,除非下文「例外」另有说明。
|
||||
|
||||
## 命名例外(不按小驼峰)
|
||||
|
||||
- **类型 / 接口 / 枚举名**:使用 PascalCase(如 `OrgInfo`、`UserStatus`)。
|
||||
- **Vue 单文件组件文件名**:PascalCase(如 `OrgAddOrUpdate.vue`)。
|
||||
- **常量**:若项目已有约定,可使用全大写下划线(如 `MAX_RETRY`);新建常量优先与现有文件保持一致。
|
||||
- **与外部协议强绑定且无法改名的字段**:仅在访问边界做映射,内部仍用 camelCase 变量承接(例如 `const userId = raw.user_id` 再避免把 `user_id` 传满业务层)。
|
||||
|
||||
## AI 行为要求
|
||||
|
||||
- 新增代码默认全部小驼峰;不要为「对齐后端蛇形」而在业务层引入蛇形变量名。
|
||||
- 与现有文件风格冲突时,以**当前文件**已有风格为准,但新增标识符仍优先 camelCase。
|
||||
17
.cursor/rules/git.mdc
Executable file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
---
|
||||
description: 自动生成或编写 Git commit 消息时使用的规范。
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
当生成 Git commit 消息时(包括点击 Git 面板的“小火花”图标自动生成),请严格遵守以下规则:
|
||||
1. **语言**:必须使用简体中文编写。
|
||||
2. **格式**:使用 Conventional Commits 规范 (例如: `feat:`, `fix:`, `docs:`, `style:`, `refactor:`, `chore:`)。
|
||||
3. **内容**:描述应简洁明了,准确反映代码变更。
|
||||
4. **示例**:
|
||||
- `feat: 添加用户登录功能`
|
||||
- `fix: 修复诗词列表加载失败的问题`
|
||||
- `docs: 更新项目说明文档`
|
||||
9
.editorconfig
Executable file
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
3
.env
Executable file
@@ -0,0 +1,3 @@
|
||||
# 所有环境通用(可被 .env.development / .env.production 覆盖)
|
||||
# 开发服务器端口(可选)
|
||||
# VITE_DEV_PORT=3000
|
||||
5
.env.development
Executable file
@@ -0,0 +1,5 @@
|
||||
# 开发环境:baseApi 固定为 /api,所有接口走 Vite 代理(避免跨域)
|
||||
VITE_APP_PROXY_TARGET=http://127.0.0.1:8083
|
||||
# manage 后端(/manage/ 前缀接口)—— 云端测试
|
||||
VITE_MANAGE_PROXY_TARGET=http://58.220.83.58:8084
|
||||
# VITE_DEV_PORT=3000
|
||||
5
.env.production
Executable file
@@ -0,0 +1,5 @@
|
||||
# 生产环境(打包时使用)
|
||||
# baseApi 固定为 /api,请求会发往同源再由 nginx 转发
|
||||
VITE_APP_PROXY_TARGET=http://jst.ainetv.cn
|
||||
# manage 后端(生产环境由 nginx 同源转发,此处仅占位)
|
||||
VITE_MANAGE_PROXY_TARGET=http://jst.ainetv.cn
|
||||
11
.env.test
Executable file
@@ -0,0 +1,11 @@
|
||||
# ===========================================
|
||||
# 环境变量示例(复制为 .env 或 .env.local 后按需修改)
|
||||
# 仅以 VITE_ 开头的变量会注入到前端代码(import.meta.env / @/config/env)
|
||||
# ===========================================
|
||||
|
||||
# 开发时代理目标(VITE_APP_PROXY_TARGET),baseApi 固定为 /api
|
||||
# VITE_APP_PROXY_TARGET=http://localhost:8080
|
||||
|
||||
# 开发服务器端口(可选,默认 3000)
|
||||
# VITE_DEV_PORT=3000
|
||||
VITE_APP_PROXY_TARGET=http://172.16.100.134:8083
|
||||
34
.gitignore
vendored
Executable file
@@ -0,0 +1,34 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
/test/unit/coverage/
|
||||
/test/e2e/reports/
|
||||
selenium-debug.log
|
||||
|
||||
# 生产环境本地配置(可覆盖 .env.production,不提交)
|
||||
.env.production.local
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
|
||||
# Skills
|
||||
**/skills/
|
||||
**/*SKILL*
|
||||
skills-lock.json
|
||||
|
||||
.codegraph
|
||||
|
||||
# 本地敏感文档(含数据库密码等,勿提交)
|
||||
项目进展总结.md
|
||||
|
||||
# 构建产物压缩包
|
||||
dist.zip
|
||||
*.zip
|
||||
70
README.md
Executable file
@@ -0,0 +1,70 @@
|
||||
# 爱视(aishi)管理后台前端
|
||||
|
||||
基于 **Vue 3**、**Vite**、**Element Plus** 的后台管理界面,与后端通过 **HTTP API** 对接。开发环境下请求统一走 Vite 代理,避免浏览器跨域问题。
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 类别 | 技术 |
|
||||
|------|------|
|
||||
| 框架 | Vue 3(Composition API、`script setup`) |
|
||||
| 构建 | Vite 8、TypeScript、`vue-tsc` |
|
||||
| UI | Element Plus、SCSS、UnoCSS |
|
||||
| 路由与状态 | Vue Router 5、Pinia |
|
||||
| HTTP | Axios、`qs` |
|
||||
| 其他 | Lodash、xlsx(导入导出)、部分图标与区域数据组件 |
|
||||
|
||||
## 环境要求
|
||||
|
||||
建议使用 **Node.js** 当前 LTS 版本;包管理统一使用 **[pnpm](https://pnpm.io/)**(安装:`npm install -g pnpm` 或见官网)。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 本地开发(默认读取 .env.development)
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## pnpm 脚本
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `pnpm dev` | 启动开发服务器(Vite) |
|
||||
| `pnpm build` | 类型检查(`vue-tsc`)后生产构建 |
|
||||
| `pnpm build:dev` | 以 `development` 模式构建 |
|
||||
| `pnpm build:test` | 以 `test` 模式构建 |
|
||||
| `pnpm preview` | 本地预览构建产物 |
|
||||
| `pnpm typecheck` | 仅执行 TypeScript / 模板类型检查 |
|
||||
|
||||
## 环境变量
|
||||
|
||||
前缀均为 `VITE_`,在 `vite.config.js` 中通过 `loadEnv` 加载。常见配置:
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `VITE_APP_PROXY_TARGET` | 开发代理目标地址(后端服务 origin),如 `http://127.0.0.1:8083` |
|
||||
| `VITE_DEV_PORT` | 开发服务器端口,未设置时默认 **3000** |
|
||||
|
||||
各环境的示例或覆盖见项目根目录 `.env`、`.env.development`、`.env.production`、`.env.test`。**不要将含密钥的 `.env` 文件提交到版本库。**
|
||||
|
||||
### 代理说明
|
||||
|
||||
开发模式下,`/api` 会代理到 `VITE_APP_PROXY_TARGET`,并将路径前缀 `/api` 重写为后端的实际前缀(见 `vite.config.js`)。接口客户端里的 `baseApi` 需与此约定一致。
|
||||
|
||||
## 目录结构(简要)
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/ # 接口封装
|
||||
├── assets/ # 静态资源
|
||||
├── components/ # 全局组件
|
||||
├── layout/ # 布局与框架页
|
||||
├── router/ # 路由
|
||||
├── stores/ # Pinia
|
||||
├── utils/ # 工具函数
|
||||
└── views/ # 业务页面(按模块划分,如 modules/sys、modules/device)
|
||||
```
|
||||
|
||||
路径别名:`@` → `src`,`@assets` → `src/assets`(见 `vite.config.js`)。
|
||||
10
auto-imports.d.ts
vendored
Executable file
@@ -0,0 +1,10 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
||||
79
components.d.ts
vendored
Executable file
@@ -0,0 +1,79 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AgentStoreSelect: typeof import('./src/components/agent-store-select/index.vue')['default']
|
||||
CardFlowFromToOrgSelect: typeof import('./src/components/card-flow-from-to-org-select/index.vue')['default']
|
||||
ChannelAgentTreeSelect: typeof import('./src/components/channel-agent-tree-select/index.vue')['default']
|
||||
ChannelSelect: typeof import('./src/components/channel-select/index.vue')['default']
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElImage: typeof import('element-plus/es')['ElImage']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElLink: typeof import('element-plus/es')['ElLink']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElOptionGroup: typeof import('element-plus/es')['ElOptionGroup']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElResult: typeof import('element-plus/es')['ElResult']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
IconSvg: typeof import('./src/components/icon-svg/index.vue')['default']
|
||||
NearExpiryUnusedCardsDialog: typeof import('./src/components/NearExpiryUnusedCardsDialog.vue')['default']
|
||||
OrgTreeSelect: typeof import('./src/components/org-tree-select/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
StoreSelect: typeof import('./src/components/store-select/index.vue')['default']
|
||||
TableTreeColumn: typeof import('./src/components/table-tree-column/index.vue')['default']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
20
db/add-jetsen-media-menu.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- ============================================================
|
||||
-- 橘视厅媒资管理菜单
|
||||
-- 目标库:as-daycard-openapi(dev-0418.sys_menu)
|
||||
-- 路由映射:menu.url='jetsen-media/list' -> src/views/modules/jetsen-media/list.vue
|
||||
-- 依赖后端接口:GET /sys/jetsen-media/{nav,tags,list,search,export}(as-daycard-openapi)
|
||||
-- ============================================================
|
||||
|
||||
-- 新建顶级目录"媒资管理" + 子菜单"橘视厅媒资"
|
||||
-- 超级管理员(org_id IS NULL) 自动拥有全部 type IN(0,1) 且 show='10' 的菜单,无需配 sys_role_menu。
|
||||
INSERT INTO `sys_menu`
|
||||
(`id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `show`, `order_num`, `created_at`, `updated_at`)
|
||||
VALUES
|
||||
(116, 0, '媒资管理', NULL, '', 0, 'Film', '10', 2, NOW(), NOW()),
|
||||
(117, 116, '橘视厅媒资', 'jetsen-media/list', '', 1, 'Film', '10', 1, NOW(), NOW());
|
||||
|
||||
-- 非超管角色授权(按需执行):
|
||||
-- INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (<ROLE_ID>, 116), (<ROLE_ID>, 117);
|
||||
|
||||
-- 回滚:
|
||||
-- DELETE FROM `sys_menu` WHERE `id` IN (116, 117);
|
||||
30
db/add-manage-menus.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- ============================================================
|
||||
-- 修复:新增 manage 菜单 天卡激活码 / 分润规则 / 分账记录
|
||||
-- 目标库:as-daycard-openapi(dev-0418.sys_menu)
|
||||
-- 路由映射:menu.url -> src/views/modules/${url}.vue
|
||||
-- ============================================================
|
||||
|
||||
-- 1) 还原 107/108 名称(避免与新菜单「分润规则/分账记录」重名)
|
||||
-- 107 原 url=settle-rule -> 名字还原为「结算规则」
|
||||
-- 108 原 url=settle-record -> 名字还原为「结算记录」
|
||||
UPDATE `sys_menu` SET `name` = '结算规则', `updated_at` = NOW() WHERE `id` = 107;
|
||||
UPDATE `sys_menu` SET `name` = '结算记录', `updated_at` = NOW() WHERE `id` = 108;
|
||||
|
||||
-- 2) 插入 3 个新菜单(续接现有最大 ID 112,使用 113~115)
|
||||
INSERT INTO `sys_menu`
|
||||
(`id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `show`, `order_num`, `created_at`, `updated_at`)
|
||||
VALUES
|
||||
(113, 100, '天卡激活码', 'manage/activation-code', '', 1, 'Ticket', '10', 13, NOW(), NOW()),
|
||||
(114, 100, '分润规则', 'manage/profit-rule', '', 1, 'Coin', '10', 14, NOW(), NOW()),
|
||||
(115, 100, '分账记录', 'manage/profit-sharing', '', 1, 'Wallet', '10', 15, NOW(), NOW());
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 超级管理员(org_id IS NULL) 自动拥有全部 type IN(0,1) 且 show='10' 的菜单,
|
||||
-- 无需配 sys_role_menu。非超管角色需显式关联:
|
||||
-- INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES
|
||||
-- (<ROLE_ID>, 113), (<ROLE_ID>, 114), (<ROLE_ID>, 115);
|
||||
|
||||
-- 回滚
|
||||
-- DELETE FROM `sys_menu` WHERE `id` IN (113, 114, 115);
|
||||
-- UPDATE `sys_menu` SET `name`='分润规则' WHERE `id`=107;
|
||||
-- UPDATE `sys_menu` SET `name`='分账记录' WHERE `id`=108;
|
||||
32
db/add-partner-menus.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
-- ============================================================
|
||||
-- 业务管理目录改名 + 第三方接口管理目录新增(合作伙伴/内容包/采购天卡)
|
||||
-- 目标库:as-daycard-openapi(sys_menu)
|
||||
-- 路由映射:menu.url -> src/views/modules/${url}.vue
|
||||
-- ============================================================
|
||||
|
||||
-- 1) id=100「合作伙伴管理」目录改名为「业务管理」
|
||||
UPDATE `sys_menu` SET `name` = '业务管理', `updated_at` = NOW() WHERE `id` = 100;
|
||||
|
||||
-- 2) 新建一级目录「第三方接口管理」(id=121, parent_id=0,与业务管理并行)
|
||||
INSERT INTO `sys_menu`
|
||||
(`id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `show`, `order_num`, `created_at`, `updated_at`)
|
||||
VALUES
|
||||
(121, 0, '第三方接口管理', NULL, '', 0, 'Setting', '10', 19, NOW(), NOW());
|
||||
|
||||
-- 3) 3 个子菜单挂到「第三方接口管理」目录下
|
||||
INSERT INTO `sys_menu`
|
||||
(`id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `show`, `order_num`, `created_at`, `updated_at`)
|
||||
VALUES
|
||||
(118, 121, '合作伙伴管理', 'manage/partner', '', 1, 'Connection', '10', 1, NOW(), NOW()),
|
||||
(119, 121, '内容包配置', 'manage/content-package', '', 1, 'Files', '10', 2, NOW(), NOW()),
|
||||
(120, 121, '采购天卡', 'manage/partner-card', '', 1, 'ShoppingCart', '10', 3, NOW(), NOW());
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 超级管理员(org_id IS NULL) 自动拥有全部 type IN(0,1) 且 show='10' 的菜单,
|
||||
-- 无需配 sys_role_menu。非超管角色需显式关联:
|
||||
-- INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES
|
||||
-- (<ROLE_ID>, 118), (<ROLE_ID>, 119), (<ROLE_ID>, 120), (<ROLE_ID>, 121);
|
||||
|
||||
-- 回滚
|
||||
-- DELETE FROM `sys_menu` WHERE `id` IN (118, 119, 120, 121);
|
||||
-- UPDATE `sys_menu` SET `name`='合作伙伴管理' WHERE `id`=100;
|
||||
4
db/cleanup-settle-menus.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- 删除冗余菜单:结算规则(107) 和 结算记录(108)
|
||||
-- 这两个视图与 分润规则(114)/分账记录(115) 调同一套 API,已删除前端文件
|
||||
DELETE FROM `sys_role_menu` WHERE `menu_id` IN (107, 108);
|
||||
DELETE FROM `sys_menu` WHERE `id` IN (107, 108);
|
||||
15
index.html
Executable file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>橘视厅酒店版会员系统</title>
|
||||
<!-- 开发环境 -->
|
||||
<link rel="shortcut icon" type="image/x-icon" href="./static/img/favicon.jpg">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
56
package.json
Executable file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "aishi",
|
||||
"version": "1.2.2",
|
||||
"type": "module",
|
||||
"author": "lrq",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build --mode development",
|
||||
"build:test": "vite build --mode test",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.17.0",
|
||||
"cos-js-sdk-v5": "^1.10.1",
|
||||
"element-china-area-data": "^6.1.0",
|
||||
"element-plus": "^2.14.1",
|
||||
"lodash": "^4.18.1",
|
||||
"pinia": "^3.0.4",
|
||||
"qs": "^6.15.2",
|
||||
"sass": "^1.100.0",
|
||||
"svg-sprite-loader": "3.7.3",
|
||||
"tesseract.js": "^7.0.0",
|
||||
"unocss": "^66.7.0",
|
||||
"vue": "^3.5.35",
|
||||
"vue-cookie": "^1.1.4",
|
||||
"vue-router": "^5.1.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^25.9.2",
|
||||
"@types/qs": "^6.15.1",
|
||||
"@unocss/vite": "^66.7.0",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@vue/language-plugin-pug": "^3.3.4",
|
||||
"@vue/language-server": "^3.3.4",
|
||||
"autoprefixer": "7.1.2",
|
||||
"typescript": "^6.0.3",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^31.1.0",
|
||||
"vite": "^8.0.16",
|
||||
"vue-loader": "^13.7.3",
|
||||
"vue-style-loader": "^3.1.2",
|
||||
"vue-template-compiler": "^2.7.16",
|
||||
"vue-tsc": "^3.3.4"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
]
|
||||
}
|
||||
7580
pnpm-lock.yaml
generated
Executable file
18
src/App.vue
Executable file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<el-config-provider :locale="zhCn">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade">
|
||||
<component :is="Component" v-if="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
export default {
|
||||
data () {
|
||||
return { zhCn }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
109
src/api/card.ts
Executable file
@@ -0,0 +1,109 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ApiResponse, PageParams, PageResult } from '@/types/api'
|
||||
|
||||
/**
|
||||
* 卡信息
|
||||
*/
|
||||
export interface CardInfo {
|
||||
id: number
|
||||
cardNo: string
|
||||
cardType: string
|
||||
status: string
|
||||
activeAt?: string
|
||||
expireAt?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡列表参数
|
||||
*/
|
||||
export interface CardListParams extends PageParams {
|
||||
cardNo?: string
|
||||
cardCode?: string
|
||||
status?: string
|
||||
cardType?: string
|
||||
inventoryStatus?: string
|
||||
usageStatus?: string
|
||||
beginActiveAt?: string
|
||||
endActiveAt?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡导入数据
|
||||
*/
|
||||
export interface CardImportData {
|
||||
cardType: string
|
||||
items: Array<{
|
||||
cardNo: string
|
||||
expireAt?: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡导入预览结果
|
||||
*/
|
||||
export interface CardImportPreview {
|
||||
total: number
|
||||
valid: number
|
||||
invalid: number
|
||||
items: Array<{
|
||||
cardNo: string
|
||||
valid: boolean
|
||||
reason?: string
|
||||
}>
|
||||
}
|
||||
|
||||
/** 卡详情 */
|
||||
export function getCardInfo(cardId: number | string): Promise<ApiResponse<CardInfo>> {
|
||||
return http.get(http.adornUrl(`/sys/card/info/${cardId}`), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 卡列表(分页) */
|
||||
export function getCardList(params?: CardListParams): Promise<ApiResponse<PageResult<CardInfo>>> {
|
||||
return http
|
||||
.get(http.adornUrl('/sys/card/list'), { params: http.adornParams(params || {}) })
|
||||
.then((res) => res.data)
|
||||
}
|
||||
|
||||
/** 卡批量导入 */
|
||||
export function importCard(data: CardImportData): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/card/import'), http.adornData(data, false))
|
||||
}
|
||||
|
||||
/** 卡预导入 */
|
||||
export function importCardPreview(data: CardImportData): Promise<ApiResponse<CardImportPreview>> {
|
||||
return http.post(http.adornUrl('/sys/card/import/preview'), http.adornData(data, false))
|
||||
}
|
||||
|
||||
/** 卡导入批次(分页) */
|
||||
export function getImportBatchList(params?: PageParams): Promise<ApiResponse<PageResult<any>>> {
|
||||
return http.get(http.adornUrl('/sys/card/import-batches'), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
|
||||
/** 某导入批次下的卡(分页) */
|
||||
export function getImportBatchCards(
|
||||
importBatchId: number | string,
|
||||
params?: PageParams
|
||||
): Promise<ApiResponse<PageResult<CardInfo>>> {
|
||||
return http.get(
|
||||
http.adornUrl(`/sys/card/import-batches/${importBatchId}/cards`),
|
||||
{ params: http.adornParams(params || {}) }
|
||||
)
|
||||
}
|
||||
|
||||
/** 临期未使用卡(剩余激活时间 ≤ 31 天) */
|
||||
export interface NearExpiryUnusedCard {
|
||||
id: number
|
||||
cardNo: string
|
||||
cardType: string
|
||||
orgId: number
|
||||
orgName: string
|
||||
activationExpireAt: string
|
||||
remainingActivationDays: number
|
||||
}
|
||||
|
||||
export function getNearExpiryUnusedCards(): Promise<ApiResponse<{ list: NearExpiryUnusedCard[] }>> {
|
||||
return http
|
||||
.get(http.adornUrl('/sys/card/near-expiry-unused'), { params: http.adornParams() })
|
||||
.then((res) => res.data)
|
||||
}
|
||||
60
src/api/cardAllocation.ts
Executable file
@@ -0,0 +1,60 @@
|
||||
import type { AxiosResponse } from 'axios'
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
|
||||
/**
|
||||
* 卡分配明细参数
|
||||
*/
|
||||
export interface CardAllocationDetailParams {
|
||||
startDate: string
|
||||
endDate: string
|
||||
orgId?: string | number
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡分配明细项
|
||||
*/
|
||||
export interface CardAllocationDetailItem {
|
||||
bizDate: string
|
||||
counterpartyOrgId: number
|
||||
counterpartyOrgName: string
|
||||
inCount: number
|
||||
outCount: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/** 详情接口 data 中可选字段(蛇形 / 驼峰会一并兼容解析) */
|
||||
export interface CardAllocationDetailPayload {
|
||||
cardFlowBreakdown?: CardAllocationDetailItem[]
|
||||
card_flow_breakdown?: CardAllocationDetailItem[]
|
||||
byType?: Array<{
|
||||
cardType: string
|
||||
cardTypeName?: string
|
||||
allocatableCount?: number
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡分配详情:data.cardFlowBreakdown(bizDate、counterpartyOrg*、inCount、outCount);可选 byType 等。
|
||||
* @param params yyyy-MM-dd;可选 orgId
|
||||
*/
|
||||
export function getCardAllocationDetail(
|
||||
params: CardAllocationDetailParams
|
||||
): Promise<AxiosResponse<ApiResponse<CardAllocationDetailPayload>>> {
|
||||
return http.get(http.adornUrl('/sys/card-allocation/detail'), {
|
||||
params: http.adornParams(params || {})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡分配详情导出(Excel);查询参数与 getCardAllocationDetail 一致。
|
||||
* @param params yyyy-MM-dd;可选 orgId
|
||||
*/
|
||||
export function exportCardAllocationDetail(
|
||||
params: CardAllocationDetailParams
|
||||
): Promise<AxiosResponse<Blob>> {
|
||||
return http.get(http.adornUrl('/sys/card-allocation/detail/export'), {
|
||||
params: http.adornParams(params || {}),
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
145
src/api/cardFlow.ts
Executable file
@@ -0,0 +1,145 @@
|
||||
import type { AxiosResponse } from 'axios'
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
|
||||
/**
|
||||
* POST /sys/card-flow/allocate — 与后端 CardFlowAllocateRequest 对齐
|
||||
*/
|
||||
export interface CardFlowAllocateRequest {
|
||||
/** 分配数量(≥1) */
|
||||
count: number
|
||||
/** YEAR / QUARTER / MONTH / WEEK / OTHER */
|
||||
cardType: string
|
||||
/** 目标渠道/代理商/门店组织 ID */
|
||||
toChannelOrgId: number
|
||||
/** 分配到门店时的权益激活码(可选,不传则服务端生成) */
|
||||
activationCode?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡转移 / 回退等通用请求体(allocate 请用 CardFlowAllocateRequest)
|
||||
*/
|
||||
export interface CardFlowData {
|
||||
cardNos?: string[]
|
||||
count?: number
|
||||
fromOrgId?: number
|
||||
toOrgId?: number
|
||||
cardType?: string
|
||||
importBatchId?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/** 卡分配/销售 */
|
||||
export function allocateCard(data: CardFlowAllocateRequest): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/card-flow/allocate'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 卡转移 */
|
||||
export function transferCard(data: CardFlowData): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/card-flow/transfer'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 卡回退(渠道/代理商按数量回退到上级,请求体:{ count, fromOrgId, cardType }) */
|
||||
export function rollbackCard(data: CardFlowData): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/card-flow/rollback-to-parent-by-count'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 无效卡按导入批次回退至渠道,请求体:{ importBatchId } */
|
||||
export function rollbackInvalidImportBatchToChannel(
|
||||
data: Pick<CardFlowData, 'importBatchId'>
|
||||
): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/card-flow/rollback-invalid-import-batch-to-channel'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** GET /sys/card-flow/allocation-orders/search 查询参数(fromOrgId / toOrgId 至少填一个) */
|
||||
export interface AllocationOrderSearchParams {
|
||||
fromOrgId?: number
|
||||
toOrgId?: number
|
||||
page?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
/** 分配单搜索列表项 */
|
||||
export interface AllocationOrderSearchItem {
|
||||
id: number
|
||||
orderNo: string
|
||||
fromOrgId: number
|
||||
fromOrgName: string
|
||||
fromOrgType: string
|
||||
toOrgId: number
|
||||
toOrgName: string
|
||||
toOrgType: string
|
||||
totalCount: number
|
||||
status: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/** 分配单搜索响应(字段可能在 data 内或响应根级) */
|
||||
export interface AllocationOrderSearchPayload {
|
||||
fromOrgId?: number
|
||||
toOrgId?: number
|
||||
items?: AllocationOrderSearchItem[]
|
||||
totalCount?: number
|
||||
page?: {
|
||||
list?: AllocationOrderSearchItem[]
|
||||
totalCount?: number
|
||||
}
|
||||
}
|
||||
|
||||
/** GET /sys/card-flow/allocation-order-final-destination-stat 查询参数 */
|
||||
export interface AllocationOrderFinalDestinationStatsParams {
|
||||
/** 分配单 ID */
|
||||
allocationOrderId: number
|
||||
page?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
/** 各组织最终去向统计行 */
|
||||
export interface AllocationOrderFinalDestinationStatItem {
|
||||
orgId: number
|
||||
orgName: string
|
||||
orgType: string
|
||||
count: number
|
||||
}
|
||||
|
||||
/** 分配单卡最终去向统计响应(字段可能在 data 内或响应根级) */
|
||||
export interface AllocationOrderFinalDestinationStatsPayload {
|
||||
allocationOrderId: number
|
||||
orderNo: string
|
||||
fromOrgId: number
|
||||
toOrgId: number
|
||||
/** 卡总数(与 cardTotalCount 同义) */
|
||||
totalCount?: number
|
||||
cardTotalCount?: number
|
||||
totalCountOnOrder?: number
|
||||
items?: AllocationOrderFinalDestinationStatItem[]
|
||||
page?: {
|
||||
list?: AllocationOrderFinalDestinationStatItem[]
|
||||
totalCount?: number
|
||||
pageSize?: number
|
||||
totalPage?: number
|
||||
currPage?: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按流出/流入组织搜索分配单
|
||||
*/
|
||||
export function searchAllocationOrders(
|
||||
params: AllocationOrderSearchParams
|
||||
): Promise<AxiosResponse<ApiResponse<AllocationOrderSearchPayload>>> {
|
||||
return http.get(http.adornUrl('/sys/card-flow/allocation-orders/search'), {
|
||||
params: http.adornParams(params)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 按分配单 ID 统计卡最终流向(card_allocation_log → card.org_id 分组)
|
||||
*/
|
||||
export function getAllocationOrderFinalDestinationStats(
|
||||
params: AllocationOrderFinalDestinationStatsParams
|
||||
): Promise<AxiosResponse<ApiResponse<AllocationOrderFinalDestinationStatsPayload>>> {
|
||||
return http.get(http.adornUrl('/sys/card-flow/allocation-order-final-destination-stats'), {
|
||||
params: http.adornParams(params)
|
||||
})
|
||||
}
|
||||
75
src/api/contract.ts
Executable file
@@ -0,0 +1,75 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ApiResponse, PageParams, PageResult } from '@/types/api'
|
||||
|
||||
/**
|
||||
* 激活码信息
|
||||
*/
|
||||
export interface ActivationCode {
|
||||
id: number
|
||||
code: string
|
||||
status: string
|
||||
maxDeviceCount?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同查询参数
|
||||
*/
|
||||
export interface ContractQueryParams extends PageParams {
|
||||
storeOrgId?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存激活码数据(常与合同页对齐:code、cardType、maxDeviceCount)
|
||||
*/
|
||||
export interface SaveActivationCodeData {
|
||||
code?: string
|
||||
maxDeviceCount?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/** 按门店查询合同/激活码列表 */
|
||||
export function getContractByStore(data: ContractQueryParams): Promise<ApiResponse<PageResult<any>>> {
|
||||
return http.post(http.adornUrl('/manage/contract/by-store'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 获取合同下激活码 */
|
||||
export function getContractActivationCode(
|
||||
storeOrgId: number
|
||||
): Promise<ApiResponse<ActivationCode[]>> {
|
||||
return http.get(http.adornUrl(`/manage/contract/${storeOrgId}/activation-code`), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 获取合同下激活码(传 contractId) */
|
||||
export function getActivationCodeByContractId(
|
||||
contractId: number
|
||||
): Promise<ApiResponse<ActivationCode[]>> {
|
||||
return http.get(http.adornUrl(`/manage/contract/${contractId}/activation-code`), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 重新生成激活码 */
|
||||
export function regenerateActivationCode(cacId: number): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl(`/manage/contract/activation-code/${cacId}/regenerate-code`), http.adornData({}))
|
||||
}
|
||||
|
||||
/** 获取激活码最大设备数 */
|
||||
export function getActivationCodeMaxDeviceCount(storeOrgId: number): Promise<ApiResponse<number>> {
|
||||
return http.get(http.adornUrl(`/manage/contract/activation-code/${storeOrgId}/max-device-count`), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 更新激活码最大设备数(PUT);路径参数为门店组织 ID,非 CAC 主键 */
|
||||
export function updateActivationCodeMaxDeviceCount(
|
||||
storeOrgId: number,
|
||||
data: { maxDeviceCount: number } & Record<string, any>
|
||||
): Promise<ApiResponse<any>> {
|
||||
return http.put(http.adornUrl(`/manage/contract/activation-code/${storeOrgId}/max-device-count`), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 创建/保存门店下权益激活码(POST) */
|
||||
export function saveStoreActivationCode(
|
||||
storeOrgId: number,
|
||||
data: SaveActivationCodeData
|
||||
): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl(`/manage/contract/${storeOrgId}/activation-code`), http.adornData(data))
|
||||
}
|
||||
231
src/api/device.ts
Executable file
@@ -0,0 +1,231 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ApiResponse, PageParams, PageResult } from '@/types/api'
|
||||
|
||||
/**
|
||||
* 设备信息
|
||||
*/
|
||||
export interface DeviceInfo {
|
||||
id: number
|
||||
deviceSn?: string
|
||||
sn?: string
|
||||
deviceUid?: string
|
||||
uid?: string
|
||||
mac?: string
|
||||
status?: string
|
||||
bindCardNo?: string | null
|
||||
bindCardActiveAt?: string | null
|
||||
bindCardPresetAt?: string | null
|
||||
bindCardExpireAt?: string | null
|
||||
bindCardStatus?: string | null
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备列表查询参数
|
||||
*/
|
||||
export interface DeviceListParams extends PageParams {
|
||||
storeOrgId?: number
|
||||
mac?: string
|
||||
cardStatus?: 'UNBOUND' | 'PRESET' | 'ACTIVATING' | 'ACTIVATION_FAILED' | 'ACTIVE' | 'EXPIRED'
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备导入数据
|
||||
*/
|
||||
export interface DeviceImportData {
|
||||
storeOrgId: number
|
||||
cardType: string
|
||||
items: Array<{
|
||||
mac?: string
|
||||
deviceSn?: string
|
||||
deviceUid?: string
|
||||
roomNo?: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备批量预置数据
|
||||
*/
|
||||
export interface DeviceBatchPresetData {
|
||||
storeOrgId: number
|
||||
cardType: string
|
||||
items: Array<{
|
||||
deviceId?: number
|
||||
mac?: string
|
||||
deviceSn?: string
|
||||
deviceUid?: string
|
||||
}>
|
||||
}
|
||||
|
||||
/** 设备列表:page、limit、storeOrgId(可选)、mac(模糊)、cardStatus */
|
||||
export function getDeviceList(params?: DeviceListParams): Promise<ApiResponse<PageResult<DeviceInfo>>> {
|
||||
return http.get(http.adornUrl('/sys/device/list'), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
|
||||
/** 导出设备列表 Excel */
|
||||
export function exportDeviceListExcel(params?: DeviceListParams): Promise<Blob> {
|
||||
return http.get(http.adornUrl('/sys/device/export-excel'), {
|
||||
params: http.adornParams(params || {}),
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
/** 重复 MAC/设备 SN 列表 */
|
||||
export function getDuplicateMacDeviceSnList(
|
||||
params?: DeviceListParams
|
||||
): Promise<ApiResponse<PageResult<DeviceInfo>>> {
|
||||
return http.get(http.adornUrl('/sys/device/duplicate-mac'), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
|
||||
/** 临期设备(年卡等即将到期)列表 */
|
||||
export function getExpiringYearDeviceList(
|
||||
params?: DeviceListParams
|
||||
): Promise<ApiResponse<PageResult<DeviceInfo>>> {
|
||||
return http.get(http.adornUrl('/sys/device/expiring-year'), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
|
||||
/** 设备列表(不按组织过滤) */
|
||||
export function getDeviceListWithoutOrg(
|
||||
params?: DeviceListParams
|
||||
): Promise<ApiResponse<PageResult<DeviceInfo>>> {
|
||||
return http.get(http.adornUrl('/sys/device/list-without-org'), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
|
||||
/** 按组织 ID 获取该组织下设备列表 */
|
||||
export function getDeviceListByOrg(orgId: number): Promise<ApiResponse<DeviceInfo[]>> {
|
||||
return http.get(http.adornUrl(`/sys/device/list-by-org/${orgId}`), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 设备批量导入 */
|
||||
export function importDevice(data: DeviceImportData): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/import'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 设备单条导入(手动添加) */
|
||||
export function importDeviceOne(data: Partial<DeviceInfo>): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/import-one'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 设备批量预置(设备授权/预置) */
|
||||
export function batchPresetDevice(data: DeviceBatchPresetData): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/batch-preset'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 批量预置/导入前服务端校验 */
|
||||
export function validateDeviceImport(data: DeviceImportData): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/import-validate'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 设备预激活 */
|
||||
export function bindDevicePreImport(data: { deviceId: number }): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/preImport'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 设备解绑 */
|
||||
export function unbindDevice(data: { deviceId: number }): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/unbind'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 预置设备删除预置 */
|
||||
export function cancelPresetDevice(data: { deviceId: number }): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/cancel-preset'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 设备间权益卡转移 */
|
||||
export function transferDeviceCard(data: {
|
||||
fromDeviceId: number
|
||||
toDeviceId: number
|
||||
}): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/transfer-card'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 可换绑的卡信息 */
|
||||
export interface ReplaceableCard {
|
||||
id: number
|
||||
cardNo: string
|
||||
thirdCardNo?: string
|
||||
cardType: string
|
||||
thirdStartAt?: string
|
||||
thirdExpireAt?: string
|
||||
}
|
||||
|
||||
/** 查询设备可换绑的卡列表 */
|
||||
export function getReplaceableCards(deviceId: number | string): Promise<ApiResponse<ReplaceableCard[]>> {
|
||||
return http.get(http.adornUrl(`/sys/device/${deviceId}/replaceable-cards`), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 设备换卡 */
|
||||
export function replaceCard(data: { deviceId: number; cardId: number }): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/replace-card'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 修改设备房间号 */
|
||||
export function updateDeviceRoomNo(data: { deviceId: number; roomNo: string }): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/update-room-no'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 修改设备 MAC */
|
||||
export function updateDeviceMac(data: { deviceId: number; mac: string }): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/update-mac'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 设备续费 */
|
||||
export function renewDevice(data: { deviceId: number; cardType: string }): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/renew'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 根据设备ID查询设备详细信息 */
|
||||
export function getDeviceDetail(deviceId: number): Promise<ApiResponse<DeviceInfo>> {
|
||||
return http.get(http.adornUrl(`/sys/device/detail/${deviceId}`), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 开发人员设备迁移 */
|
||||
export function developerManualImportDevice(data: {
|
||||
storeId: number
|
||||
mac: string
|
||||
expireAt: string
|
||||
}): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/developer-manual-import'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 设备转移门店 */
|
||||
export function assignDeviceStore(data: {
|
||||
deviceId: number
|
||||
toStoreOrgId?: number
|
||||
storeOrgId?: number
|
||||
}): Promise<ApiResponse<any>> {
|
||||
const payload = {
|
||||
deviceId: data?.deviceId,
|
||||
toStoreOrgId: data?.toStoreOrgId ?? data?.storeOrgId
|
||||
}
|
||||
return http.post(http.adornUrl('/sys/device/transform'), http.adornData(payload))
|
||||
}
|
||||
|
||||
/** 清空设备 SN/UID */
|
||||
export function clearDeviceSnUid(data: { deviceId: number }): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/clear-sn-uid'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 软删除设备 */
|
||||
export function softDeleteDevice(data: { deviceId: number }): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/soft-delete'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 禁用设备(status → DISABLED) */
|
||||
export function disableDevice(data: { deviceId: number }): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/disable'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 同 MAC 已绑卡设备列表(不含自身,字段与 duplicate-mac 列表一致) */
|
||||
export function getSameMacBoundDeviceList(deviceId: number): Promise<ApiResponse<DeviceInfo[]>> {
|
||||
return http.get(http.adornUrl(`/sys/device/same-mac-bound-list/${deviceId}`), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 替换重复设备:保留 device1 绑卡,交换 SN/UID,软删除 device2 */
|
||||
export function replaceDuplicateDevice(data: {
|
||||
device1: number
|
||||
device2: number
|
||||
}): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/device/replace-duplicate'), http.adornData(data))
|
||||
}
|
||||
22
src/api/index.ts
Executable file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 统一导出 http 实例及所有 API 模块
|
||||
* 业务请从各模块按需引入,例如:import { getOrgTree } from '@/api/org'
|
||||
*
|
||||
* 新 manage 后端接口(/manage/ 前缀)请从独立入口引入:
|
||||
* import { getChannelList, getOverviewStats } from '@/api/manage'
|
||||
* 或按模块引入:
|
||||
* import { getChannelList } from '@/api/manage/channel'
|
||||
*/
|
||||
import http from '@/utils/httpRequest'
|
||||
|
||||
export { http }
|
||||
export * from './org'
|
||||
export * from './card'
|
||||
export * from './cardFlow'
|
||||
export * from './cardAllocation'
|
||||
export * from './contract'
|
||||
export * from './store'
|
||||
export * from './device'
|
||||
export * from './mediaAsset'
|
||||
export * from './mediaAssetRank'
|
||||
export * from './sys'
|
||||
83
src/api/jetsenMedia.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
|
||||
/**
|
||||
* 橘视厅媒资内容项(对应后端透传的橘视厅 result 字段)
|
||||
*/
|
||||
export interface JetsenContentItem {
|
||||
contentcode: string
|
||||
name: string
|
||||
categoryname?: string
|
||||
score?: string
|
||||
actordisplay?: string
|
||||
description?: string
|
||||
releaseyear?: string
|
||||
originalcountry?: string
|
||||
isFree?: number
|
||||
episodecount?: number
|
||||
duration?: string
|
||||
posterimg?: string
|
||||
midimg?: string
|
||||
image?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/** 栏目标签 */
|
||||
export interface JetsenTag {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
type?: number
|
||||
parent_id?: number
|
||||
big_project?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/** 栏目树节点(一级含 children 二级子栏目) */
|
||||
export interface JetsenCatalogNode {
|
||||
id: number
|
||||
name: string
|
||||
code?: string
|
||||
children?: JetsenCatalogNode[]
|
||||
}
|
||||
|
||||
/** 分页数据(后端 R.dataSuccess 统一格式) */
|
||||
export interface JetsenPageData<T> {
|
||||
list: T[]
|
||||
totalCount: number
|
||||
currPage: number
|
||||
pageSize: number
|
||||
totalPage: number
|
||||
}
|
||||
|
||||
/** 首页导航(一级栏目入口) */
|
||||
export function getJetsenMediaNav() {
|
||||
return http.get(http.adornUrl('/manage/jetsen-media/nav'), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 栏目标签(一级栏目传 parentid=0) */
|
||||
export function getJetsenMediaTags(parentid: string | number = 0) {
|
||||
return http.get(http.adornUrl('/manage/jetsen-media/tags'), { params: http.adornParams({ parentid }) })
|
||||
}
|
||||
|
||||
/** 栏目树(一级栏目 + 其下二级子栏目,查内容列表用二级 id) */
|
||||
export function getJetsenMediaCatalogTree() {
|
||||
return http.get(http.adornUrl('/manage/jetsen-media/catalog-tree'), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 内容列表(按栏目 catalogid 分页) */
|
||||
export function getJetsenMediaList(params: { catalogid: string | number; pageindex: number; pagesize: number }) {
|
||||
return http.get(http.adornUrl('/manage/jetsen-media/list'), { params: http.adornParams(params) })
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
export function searchJetsenMedia(params: { keyword: string; pageindex: number; pagesize: number }) {
|
||||
return http.get(http.adornUrl('/manage/jetsen-media/search'), { params: http.adornParams(params) })
|
||||
}
|
||||
|
||||
/** 导出 Excel(返回 blob) */
|
||||
export function exportJetsenMedia(params: { catalogid?: string | number; keyword?: string; catalogName?: string }) {
|
||||
return http.get(http.adornUrl('/manage/jetsen-media/export'), {
|
||||
params: http.adornParams(params),
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
55
src/api/manage/activationCode.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageResponse } from './types'
|
||||
|
||||
// ===================== 天卡设备激活码 =====================
|
||||
|
||||
/** 激活码实体 */
|
||||
export interface ActivationCodeItem {
|
||||
id: number
|
||||
/** 激活码(5位字符,如 7A3FK) */
|
||||
code: string
|
||||
/** 渠道组织ID */
|
||||
channelOrgId: number
|
||||
/** 门店组织ID */
|
||||
storeOrgId: number
|
||||
/** 状态:ACTIVE-启用 / DISABLED-禁用 */
|
||||
status: 'ACTIVE' | 'DISABLED'
|
||||
/** 最大可激活设备数(0=不限) */
|
||||
maxDeviceCount: number
|
||||
/** 已激活设备数 */
|
||||
activatedCount: number
|
||||
/** 备注 */
|
||||
remark: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/** 生成激活码请求体 */
|
||||
export interface ActivationCodeGenerateBody {
|
||||
storeOrgId: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/** 按门店查询激活码列表(GET /manage/daycard/activation-code/list/{storeOrgId}) */
|
||||
export function getActivationCodeList (storeOrgId: number): Promise<ManageResponse<{ list: ActivationCodeItem[] }>> {
|
||||
return http.get(http.adornUrl(`/manage/daycard/activation-code/list/${storeOrgId}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 为门店生成激活码(每门店一个,已存在则返回已有的) */
|
||||
export function generateActivationCode (data: ActivationCodeGenerateBody): Promise<ManageResponse<ActivationCodeItem>> {
|
||||
return http.post(http.adornUrl('/manage/daycard/activation-code/generate'), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 重新生成激活码(旧码失效) */
|
||||
export function regenerateActivationCode (id: number): Promise<ManageResponse<{ code: string }>> {
|
||||
return http.post(http.adornUrl(`/manage/daycard/activation-code/${id}/regenerate`), http.adornData({})).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 启用/禁用激活码(PUT /manage/daycard/activation-code/{id}/status) */
|
||||
export function updateActivationCodeStatus (id: number, status: 'ACTIVE' | 'DISABLED'): Promise<ManageResponse> {
|
||||
return http.put(http.adornUrl(`/manage/daycard/activation-code/${id}/status`), http.adornData({ status })).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 删除激活码(软删除) */
|
||||
export function deleteActivationCode (id: number): Promise<ManageResponse> {
|
||||
return http.delete(http.adornUrl(`/manage/daycard/activation-code/${id}`)).then(res => res.data)
|
||||
}
|
||||
75
src/api/manage/channel.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageListResponse, ManageResponse, ManagePageParams } from './types'
|
||||
|
||||
/**
|
||||
* 渠道信息
|
||||
*/
|
||||
export interface ChannelInfo {
|
||||
id: number
|
||||
parentId: number
|
||||
mpath: string
|
||||
name: string
|
||||
orderNo?: number
|
||||
type: string
|
||||
/** 后端 @JsonProperty("orgCode") 重命名,JSON 中为 orgCode */
|
||||
orgCode?: string
|
||||
canHoldInventory: boolean
|
||||
activationPrefix: string
|
||||
brandLogoUrl: string | null
|
||||
developmentStaff?: string
|
||||
contactPhone?: string
|
||||
servicePartner?: string
|
||||
wechatQrImageUrl?: string | null
|
||||
status: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
/** 非表字段,后端 join 返回 */
|
||||
parentOrgName?: string
|
||||
channelName?: string
|
||||
children?: ChannelInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 渠道列表查询参数
|
||||
*/
|
||||
export interface ChannelListParams extends ManagePageParams {
|
||||
name?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 渠道保存/更新数据
|
||||
*/
|
||||
export interface ChannelSaveData {
|
||||
id?: number
|
||||
parentId: number
|
||||
name: string
|
||||
orgCode?: string
|
||||
canHoldInventory?: boolean
|
||||
activationPrefix?: string
|
||||
brandLogoUrl?: string
|
||||
developmentStaff?: string
|
||||
contactPhone?: string
|
||||
servicePartner?: string
|
||||
}
|
||||
|
||||
/** 渠道分页列表 */
|
||||
export function getChannelList(params?: ChannelListParams): Promise<ManageListResponse<ChannelInfo>> {
|
||||
return http.get(http.adornUrl('/manage/org/channel/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 渠道详情 */
|
||||
export function getChannelInfo(id: number): Promise<ManageResponse<{ channel?: ChannelInfo }>> {
|
||||
return http.get(http.adornUrl(`/manage/org/channel/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 新增/修改渠道 */
|
||||
export function saveOrUpdateChannel(data: ChannelSaveData): Promise<ManageResponse> {
|
||||
const url = data.id ? '/manage/org/channel/update' : '/manage/org/channel/save'
|
||||
return http.post(http.adornUrl(url), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 删除渠道(批量软删除) */
|
||||
export function deleteChannel(ids: number[]): Promise<ManageResponse> {
|
||||
return http.post(http.adornUrl('/manage/org/channel/delete'), http.adornData(ids, false)).then(res => res.data)
|
||||
}
|
||||
112
src/api/manage/content-package.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageListResponse, ManageResponse, ManagePageParams } from './types'
|
||||
|
||||
/**
|
||||
* 内容包信息(绑定 partnerId)
|
||||
*/
|
||||
export interface ContentPackage {
|
||||
id: number
|
||||
/** 合作伙伴UUID(字符串) */
|
||||
partnerId: string
|
||||
name: string
|
||||
description?: string
|
||||
/** 状态:ACTIVE-启用 / DISABLED-禁用 */
|
||||
status: 'ACTIVE' | 'DISABLED'
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
/** 非表字段,后端 join 返回 */
|
||||
partnerName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容包保存/更新数据
|
||||
*/
|
||||
export interface ContentPackageSaveData {
|
||||
id?: number
|
||||
/** 合作伙伴UUID(字符串) */
|
||||
partnerId: string
|
||||
name: string
|
||||
description?: string
|
||||
status?: 'ACTIVE' | 'DISABLED'
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容包列表查询参数
|
||||
*/
|
||||
export interface ContentPackageListParams extends ManagePageParams {
|
||||
/** 合作伙伴UUID(字符串) */
|
||||
partnerId?: string
|
||||
name?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
/** 内容包分页列表 */
|
||||
export function getContentPackageList(params?: ContentPackageListParams): Promise<ManageListResponse<ContentPackage>> {
|
||||
return http.get(http.adornUrl('/manage/content-package/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
// ===================== 栏目内容选择辅助接口 =====================
|
||||
|
||||
/** 栏目内容预览项(精简字段,供管理员勾选) */
|
||||
export interface CatalogContentItem {
|
||||
contentcode: string
|
||||
name: string
|
||||
posterimg?: string
|
||||
categoryname?: string
|
||||
score?: string
|
||||
isFree?: number | string
|
||||
episodecount?: number
|
||||
duration?: number
|
||||
releaseyear?: string
|
||||
originalcountry?: string
|
||||
}
|
||||
|
||||
/** 内容包详情(含解析后的内容列表,前端直接展示,无需 JSON.parse) */
|
||||
export interface ContentPackageDetail {
|
||||
/** 原始实体(contentCodes 是 JSON 字符串) */
|
||||
contentPackage: ContentPackage
|
||||
/** 解析后的内容数组 */
|
||||
contentList: CatalogContentItem[]
|
||||
}
|
||||
|
||||
/** 内容包详情(按 id,含 contentList) */
|
||||
export function getContentPackageInfo(id: number): Promise<ManageResponse<ContentPackageDetail>> {
|
||||
return http.get(http.adornUrl(`/manage/content-package/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 内容包详情(按 partnerId,含 contentList) */
|
||||
export function getContentPackageInfoByPartner(partnerId: string): Promise<ManageResponse<ContentPackageDetail>> {
|
||||
return http.get(http.adornUrl(`/manage/content-package/info-by-partner/${partnerId}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 新增/修改内容包元信息(绑定 partnerId) */
|
||||
export function saveOrUpdateContentPackage(data: ContentPackageSaveData): Promise<ManageResponse> {
|
||||
const url = data.id ? '/manage/content-package/update' : '/manage/content-package/save'
|
||||
return http.post(http.adornUrl(url), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 删除内容包(软删除,批量) */
|
||||
export function deleteContentPackage(ids: number[]): Promise<ManageResponse> {
|
||||
return http.post(http.adornUrl('/manage/content-package/delete'), http.adornData(ids, false)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 拉取栏目内容预览(GET /manage/content-package/fetch-contents?catalogid=&pagesize=) */
|
||||
export function fetchPackageContents(params: { catalogid: string | number; pagesize?: number; pageindex?: number }): Promise<ManageResponse<{ list: CatalogContentItem[]; totalCount: number; totalPage: number }>> {
|
||||
return http.get(http.adornUrl('/manage/content-package/fetch-contents'), { params: http.adornParams(params) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 一键保存勾选内容请求体(POST /manage/content-package/save-from-selection;传 id 覆盖,不传新建) */
|
||||
export interface SaveFromSelectionBody {
|
||||
/** 传 id 按 id 更新覆盖;不传按 partnerId 查找 */
|
||||
id?: number
|
||||
/** 合作伙伴UUID(字符串) */
|
||||
partnerId: string
|
||||
name: string
|
||||
catalogIds: string
|
||||
selectedContents: CatalogContentItem[]
|
||||
}
|
||||
|
||||
/** 一键保存勾选内容(管理员勾选的内容对象数组直接传后端,序列化存入 content_codes;不带 t 时间戳) */
|
||||
export function saveContentPackageFromSelection(data: SaveFromSelectionBody): Promise<ManageResponse> {
|
||||
return http.post(http.adornUrl('/manage/content-package/save-from-selection'), http.adornData(data, false)).then(res => res.data)
|
||||
}
|
||||
151
src/api/manage/daycard.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageListResponse, ManageResponse, ManagePageParams } from './types'
|
||||
|
||||
// ===================== 天卡池 =====================
|
||||
|
||||
/** 天卡池实体 */
|
||||
export interface DayCardPoolItem {
|
||||
id: number
|
||||
/** 卡密(橘视厅激活码) */
|
||||
cardCode: string
|
||||
/** 第三方引用标识(橘视厅免密登录 uid) */
|
||||
thirdPartyRef: string
|
||||
/** 第三方卡号(D + 24位随机,App 用于访问内容) */
|
||||
thirdCardNo: string
|
||||
cardType: string
|
||||
partnerId: number
|
||||
/** 状态: UNUSED / RESERVED / USED */
|
||||
status: string
|
||||
/** 激活状态: PENDING / SUCCESS / FAILED */
|
||||
activateStatus: string
|
||||
activateError: string | null
|
||||
activatedAt: string | null
|
||||
orderNo: string | null
|
||||
deviceId: number | null
|
||||
importBatchId: number | null
|
||||
expireAt: string | null
|
||||
usedAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/** 卡池列表参数 */
|
||||
export interface DayCardPoolListParams extends ManagePageParams {
|
||||
partnerId?: number
|
||||
status?: string
|
||||
cardCode?: string
|
||||
orderNo?: string
|
||||
}
|
||||
|
||||
/** 卡池统计 */
|
||||
export interface DayCardPoolStats {
|
||||
total: number
|
||||
available: number
|
||||
reserved: number
|
||||
used: number
|
||||
}
|
||||
|
||||
/** 导入卡密请求 */
|
||||
export interface DayCardPoolImportData {
|
||||
partnerId: number
|
||||
cardCodes: string[]
|
||||
}
|
||||
|
||||
/** 导入卡密响应 */
|
||||
export interface DayCardPoolImportResult {
|
||||
successCount: number
|
||||
totalCount: number
|
||||
}
|
||||
|
||||
/** 导入批次 */
|
||||
export interface DayCardImportBatch {
|
||||
id: number
|
||||
batchNo: string
|
||||
partnerId: number
|
||||
status: string
|
||||
totalCount: number
|
||||
successCount: number
|
||||
failCount: number
|
||||
remark: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/** 获取天卡池列表 */
|
||||
export function getDayCardPoolList (params?: DayCardPoolListParams): Promise<ManageListResponse<DayCardPoolItem>> {
|
||||
return http.get(http.adornUrl('/manage/daycard/pool/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 获取卡池统计 */
|
||||
export function getDayCardPoolStats (partnerId?: number): Promise<ManageResponse<{ stats?: DayCardPoolStats }>> {
|
||||
return http.get(http.adornUrl('/manage/daycard/pool/stats'), { params: http.adornParams({ partnerId }) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 导入卡密 */
|
||||
export function importDayCardPool (data: DayCardPoolImportData): Promise<ManageResponse<DayCardPoolImportResult>> {
|
||||
return http.post(http.adornUrl('/manage/daycard/pool/import'), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
// ===================== 天卡订单 =====================
|
||||
|
||||
/** 天卡订单实体 */
|
||||
export interface DayCardOrderItem {
|
||||
id: number
|
||||
orderNo: string
|
||||
partnerId: number
|
||||
deviceId: number
|
||||
storeOrgId: number
|
||||
channelOrgId: number
|
||||
/** 卡号(值为 DayCardPoolEntity.cardCode) */
|
||||
cardNo: string | null
|
||||
/** 订单状态: CREATED / PAYING / PAID / PAY_FAILED / CANCELLED */
|
||||
status: string
|
||||
amount: number
|
||||
/** 支付渠道: WECHAT 等 */
|
||||
payChannel: string | null
|
||||
/** 微信支付二维码链接 */
|
||||
codeUrl: string | null
|
||||
payNo: string | null
|
||||
payAmount: number | null
|
||||
payTime: string | null
|
||||
expireTime: string | null
|
||||
cancelTime: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
/** 非表字段 */
|
||||
partnerName?: string
|
||||
deviceMac?: string
|
||||
}
|
||||
|
||||
/** 订单列表参数 */
|
||||
export interface DayCardOrderListParams extends ManagePageParams {
|
||||
partnerId?: number
|
||||
orderNo?: string
|
||||
status?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
/** 获取订单列表 */
|
||||
export function getDayCardOrderList (params?: DayCardOrderListParams): Promise<ManageListResponse<DayCardOrderItem>> {
|
||||
return http.get(http.adornUrl('/manage/daycard/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 获取订单详情 */
|
||||
export function getDayCardOrderInfo (orderNo: string): Promise<ManageResponse<{ order?: DayCardOrderItem }>> {
|
||||
return http.get(http.adornUrl(`/manage/daycard/info/${orderNo}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
// ===================== 设备权益 =====================
|
||||
|
||||
/** 设备天卡权益 */
|
||||
export interface DeviceBenefits {
|
||||
hasBenefits: boolean
|
||||
thirdCardNo?: string
|
||||
cardCode?: string
|
||||
expireAt?: string
|
||||
}
|
||||
|
||||
/** 查询设备天卡权益 */
|
||||
export function getDeviceBenefits (deviceId: number): Promise<ManageResponse<DeviceBenefits>> {
|
||||
return http.get(http.adornUrl(`/manage/daycard/benefits/${deviceId}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
117
src/api/manage/device.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageListResponse, ManageResponse, ManagePageParams } from './types'
|
||||
|
||||
/**
|
||||
* 终端/设备信息
|
||||
*/
|
||||
export interface DeviceInfo {
|
||||
id: number
|
||||
storeOrgId: number
|
||||
mac: string
|
||||
deviceSn: string
|
||||
wifiMac?: string
|
||||
deviceUid?: string
|
||||
androidId?: string
|
||||
manufacturer?: string
|
||||
model?: string
|
||||
brand?: string
|
||||
identifierStrategy?: string
|
||||
deviceName: string
|
||||
deviceModel: string
|
||||
verCode?: string
|
||||
verName?: string
|
||||
androidVersion?: string
|
||||
roomNo: string
|
||||
lastHeartbeat?: string
|
||||
remark: string
|
||||
status: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
/** 非表字段,后端 join 返回 */
|
||||
storeName?: string
|
||||
channelOrgId?: number
|
||||
channelName?: string
|
||||
agentOrgName?: string
|
||||
/** 设备来源:PLATFORM-平台 / PARTNER-第三方接口注册 */
|
||||
source?: string
|
||||
/** 合作伙伴UUID(source=PARTNER 时) */
|
||||
partnerId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 终端列表查询参数
|
||||
*/
|
||||
export interface DeviceListParams extends ManagePageParams {
|
||||
storeOrgId?: number
|
||||
status?: string
|
||||
deviceName?: string
|
||||
mac?: string
|
||||
/** 设备来源:PLATFORM / PARTNER */
|
||||
source?: string
|
||||
/** 合作伙伴UUID(source=PARTNER 时筛选) */
|
||||
partnerId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 终端保存/更新数据
|
||||
*/
|
||||
export interface DeviceSaveData {
|
||||
id?: number
|
||||
storeOrgId: number
|
||||
mac?: string
|
||||
wifiMac?: string
|
||||
deviceUid?: string
|
||||
deviceSn?: string
|
||||
deviceName?: string
|
||||
deviceModel?: string
|
||||
roomNo?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/** 终端分页列表 */
|
||||
export function getDeviceList(params?: DeviceListParams): Promise<ManageListResponse<DeviceInfo>> {
|
||||
return http.get(http.adornUrl('/manage/device/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 终端详情 */
|
||||
export function getDeviceInfo(id: number): Promise<ManageResponse<{ device?: DeviceInfo }>> {
|
||||
return http.get(http.adornUrl(`/manage/device/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 新增/修改终端 */
|
||||
export function saveOrUpdateDevice(data: DeviceSaveData): Promise<ManageResponse> {
|
||||
const url = data.id ? '/manage/device/update' : '/manage/device/save'
|
||||
return http.post(http.adornUrl(url), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 删除终端(批量软删除) */
|
||||
export function deleteDevice(ids: number[]): Promise<ManageResponse> {
|
||||
return http.post(http.adornUrl('/manage/device/delete'), http.adornData(ids, false)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 绑定门店(query 参数传 deviceId 和 storeOrgId) */
|
||||
export function bindDeviceStore(deviceId: number, storeOrgId: number): Promise<ManageResponse> {
|
||||
return http.post(
|
||||
http.adornUrl('/manage/device/bindStore'),
|
||||
http.adornData({}),
|
||||
{ params: http.adornParams({ deviceId, storeOrgId }) }
|
||||
).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 解绑设备(换场所):清门店关联 + 作废 sign + 释放天卡权益 */
|
||||
export function unbindDeviceOrg(deviceId: number): Promise<ManageResponse> {
|
||||
return http.post(
|
||||
http.adornUrl('/manage/daycard/device/unbind-org'),
|
||||
http.adornData({}),
|
||||
{ params: http.adornParams({ deviceId }) }
|
||||
).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 退天卡:仅退卡,不解绑门店 */
|
||||
export function unbindDayCard(deviceId: number): Promise<ManageResponse> {
|
||||
return http.post(
|
||||
http.adornUrl('/manage/daycard/device/unbind'),
|
||||
http.adornData({}),
|
||||
{ params: http.adornParams({ deviceId }) }
|
||||
).then(res => res.data)
|
||||
}
|
||||
28
src/api/manage/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* /manage/ 接口模块统一导出
|
||||
*
|
||||
* 业务请从各模块按需引入,例如:
|
||||
* import { getChannelList } from '@/api/manage/channel'
|
||||
* import { getOverviewStats } from '@/api/manage/stats'
|
||||
*
|
||||
* 或从统一入口引入:
|
||||
* import { getChannelList, getOverviewStats } from '@/api/manage'
|
||||
*/
|
||||
export * from './types'
|
||||
export * from './channel'
|
||||
export * from './store'
|
||||
export * from './orgTree'
|
||||
export * from './device'
|
||||
export * from './payConfig'
|
||||
export * from './price'
|
||||
export * from './stats'
|
||||
export * from './daycard'
|
||||
export * from './activationCode'
|
||||
export * from './profitRule'
|
||||
export * from './profitReceiver'
|
||||
export * from './profitSharing'
|
||||
export * from './refund'
|
||||
export * from './partner'
|
||||
export * from './content-package'
|
||||
export * from './partner-card'
|
||||
export * from './openapiCallLog'
|
||||
42
src/api/manage/openapiCallLog.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageListResponse, ManagePageParams } from './types'
|
||||
|
||||
/**
|
||||
* OpenAPI 调用日志(第三方接口审计日志)
|
||||
*/
|
||||
export interface OpenApiCallLogItem {
|
||||
id: number
|
||||
/** 合作伙伴UUID */
|
||||
partnerId: string
|
||||
/** 应用Key */
|
||||
appKey: string
|
||||
/** 接口路径 */
|
||||
api: string
|
||||
/** 请求方法:GET / POST */
|
||||
httpMethod: string
|
||||
/** 请求参数(consume 记录设备标识,media 记录 query string) */
|
||||
params: string
|
||||
/** 结果码:0=成功,400/500=失败 */
|
||||
resultCode: number
|
||||
/** 调用方IP */
|
||||
ip: string
|
||||
/** 耗时(毫秒) */
|
||||
costMs: number
|
||||
/** 调用时间 */
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询参数
|
||||
*/
|
||||
export interface OpenApiCallLogListParams extends ManagePageParams {
|
||||
/** 合作伙伴UUID */
|
||||
partnerId?: string
|
||||
/** 接口路径(模糊查询) */
|
||||
api?: string
|
||||
}
|
||||
|
||||
/** OpenAPI 调用日志列表(第三方接口审计日志) */
|
||||
export function getOpenApiCallLogList(params?: OpenApiCallLogListParams): Promise<ManageListResponse<OpenApiCallLogItem>> {
|
||||
return http.get(http.adornUrl('/manage/openapi/call-log/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
22
src/api/manage/orgTree.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageResponse } from './types'
|
||||
|
||||
/**
|
||||
* 组织树节点
|
||||
*/
|
||||
export interface OrgTreeNode {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
mpath: string
|
||||
code?: string
|
||||
children: OrgTreeNode[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整组织树
|
||||
* 层级:PLATFORM → CHANNEL → SERVICE_PROVIDER → STORE
|
||||
*/
|
||||
export function getOrgTree(): Promise<ManageResponse<{ tree?: OrgTreeNode[] }>> {
|
||||
return http.get(http.adornUrl('/manage/org/tree'), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
98
src/api/manage/partner-card.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageListResponse, ManageResponse, ManagePageParams } from './types'
|
||||
|
||||
/**
|
||||
* 合作伙伴天卡(卡密级别)
|
||||
*/
|
||||
export interface PartnerCardItem {
|
||||
id: number
|
||||
/** 合作伙伴UUID(字符串) */
|
||||
partnerId: string
|
||||
/** 平台天卡池ID(day_card_pool.id) */
|
||||
poolId: number
|
||||
/** 天卡卡密 */
|
||||
cardCode: string
|
||||
/** 橘视厅第三方卡号 */
|
||||
thirdCardNo: string
|
||||
/** 状态:AVAILABLE-可用 / CONSUMED-已消耗 / DISABLED-已禁用 */
|
||||
status: 'AVAILABLE' | 'CONSUMED' | 'DISABLED'
|
||||
/** 消耗后绑定的设备ID(未消耗=null) */
|
||||
deviceId: number | null
|
||||
/** 渠道机构ID(消耗时传入,未消耗=null) */
|
||||
channelOrgId: number | null
|
||||
/** 采购批次号(同批次采购的天卡批次号相同) */
|
||||
purchaseBatch: string
|
||||
/** 消耗时间(未消耗=null) */
|
||||
consumedAt: string | null
|
||||
deleteFlag: string
|
||||
/** 创建时间(采购时间) */
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 库存统计
|
||||
*/
|
||||
export interface PartnerCardStats {
|
||||
total: number
|
||||
available: number
|
||||
used: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 库存列表查询参数
|
||||
*/
|
||||
export interface PartnerCardListParams extends ManagePageParams {
|
||||
/** 合作伙伴UUID(字符串) */
|
||||
partnerId?: string
|
||||
/** 状态:AVAILABLE / CONSUMED / DISABLED */
|
||||
status?: string
|
||||
/** 采购批次号 */
|
||||
purchaseBatch?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 采购天卡请求体(后端 @RequestParam,参数走 URL query)
|
||||
*/
|
||||
export interface PartnerCardPurchaseBody {
|
||||
/** 合作伙伴UUID(字符串) */
|
||||
partnerId: string
|
||||
/** 采购卡数 */
|
||||
quantity: number
|
||||
/** 金额(分) */
|
||||
amount?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/** 天卡库存列表(卡密级别) */
|
||||
export function getPartnerCardList(params?: PartnerCardListParams): Promise<ManageListResponse<PartnerCardItem>> {
|
||||
return http.get(http.adornUrl('/manage/partner-card/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 采购天卡(划拨天卡到 partner 池子;后端 @RequestParam,参数走 URL query,不放 JSON body) */
|
||||
export function purchasePartnerCard(data: PartnerCardPurchaseBody): Promise<ManageResponse> {
|
||||
return http.post(
|
||||
http.adornUrl('/manage/partner-card/purchase'),
|
||||
http.adornData({}),
|
||||
{ params: http.adornParams({ partnerId: data.partnerId, quantity: data.quantity, amount: data.amount, remark: data.remark }, false) }
|
||||
).then(res => res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 回退天卡请求体(后端 @RequestParam,参数走 URL query)
|
||||
*/
|
||||
export interface PartnerCardRevertBody {
|
||||
/** 合作伙伴UUID(字符串) */
|
||||
partnerId: string
|
||||
/** 回退数量 */
|
||||
quantity: number
|
||||
}
|
||||
|
||||
/** 回退天卡(从 partner 池取 N 张 AVAILABLE 天卡回退;平台池 RESERVED->UNUSED,partner 池卡 DISABLED;@RequestParam,URL query) */
|
||||
export function revertPartnerCard(data: PartnerCardRevertBody): Promise<ManageResponse<{ reverted: number }>> {
|
||||
return http.post(
|
||||
http.adornUrl('/manage/partner-card/revert'),
|
||||
http.adornData({}),
|
||||
{ params: http.adornParams({ partnerId: data.partnerId, quantity: data.quantity }, false) }
|
||||
).then(res => res.data)
|
||||
}
|
||||
67
src/api/manage/partner.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageListResponse, ManageResponse, ManagePageParams } from './types'
|
||||
|
||||
/**
|
||||
* 合作伙伴信息
|
||||
*/
|
||||
export interface PartnerInfo {
|
||||
id: number
|
||||
/** 合作伙伴业务UUID(16位字符串,传给内容包/采购等接口用此字段,非自增 id) */
|
||||
partnerId: string
|
||||
name: string
|
||||
/** 应用标识(新增后由后端自动生成) */
|
||||
appKey: string
|
||||
/** 应用密钥(新增后由后端自动生成,仅明文返回一次) */
|
||||
appSecret: string
|
||||
/** 状态:ACTIVE-启用 / DISABLED-禁用(禁用后 appKey 签名校验失败) */
|
||||
status: 'ACTIVE' | 'DISABLED'
|
||||
/** 限流(次/分钟,0=不限) */
|
||||
rateLimit?: number
|
||||
remark?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 合作伙伴保存/更新数据
|
||||
*/
|
||||
export interface PartnerSaveData {
|
||||
id?: number
|
||||
name: string
|
||||
status?: 'ACTIVE' | 'DISABLED'
|
||||
rateLimit?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 合作伙伴列表查询参数
|
||||
*/
|
||||
export interface PartnerListParams extends ManagePageParams {
|
||||
name?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
/** 合作伙伴分页列表 */
|
||||
export function getPartnerList(params?: PartnerListParams): Promise<ManageListResponse<PartnerInfo>> {
|
||||
return http.get(http.adornUrl('/manage/partner/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 合作伙伴详情(含 appKey/appSecret 明文) */
|
||||
export function getPartnerInfo(id: number): Promise<ManageResponse<PartnerInfo>> {
|
||||
return http.get(http.adornUrl(`/manage/partner/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 新增合作伙伴(自动生成 appKey/appSecret,返回在 data 中) */
|
||||
export function savePartner(data: PartnerSaveData): Promise<ManageResponse<PartnerInfo>> {
|
||||
return http.post(http.adornUrl('/manage/partner/save'), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 修改合作伙伴(改名称/状态/限流等;status=DISABLED 禁用) */
|
||||
export function updatePartner(data: PartnerSaveData): Promise<ManageResponse> {
|
||||
return http.post(http.adornUrl('/manage/partner/update'), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 删除合作伙伴(软删除,批量) */
|
||||
export function deletePartner(ids: number[]): Promise<ManageResponse> {
|
||||
return http.post(http.adornUrl('/manage/partner/delete'), http.adornData(ids, false)).then(res => res.data)
|
||||
}
|
||||
63
src/api/manage/payConfig.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageListResponse, ManageResponse, ManagePageParams } from './types'
|
||||
|
||||
/**
|
||||
* 微信支付配置信息
|
||||
*/
|
||||
export interface PayConfigInfo {
|
||||
id: number
|
||||
partnerId: number | null
|
||||
appId: string
|
||||
mchId: string
|
||||
apiV3Key: string
|
||||
certSerialNo: string
|
||||
privateKeyPath: string
|
||||
notifyUrl: string
|
||||
status: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付配置列表查询参数
|
||||
*/
|
||||
export interface PayConfigListParams extends ManagePageParams {
|
||||
partnerId?: number
|
||||
status?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付配置保存/更新数据
|
||||
*/
|
||||
export interface PayConfigSaveData {
|
||||
id?: number
|
||||
partnerId?: number
|
||||
appId: string
|
||||
mchId: string
|
||||
apiV3Key: string
|
||||
certSerialNo?: string
|
||||
privateKeyPath?: string
|
||||
notifyUrl: string
|
||||
status?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/** 支付配置分页列表 */
|
||||
export function getPayConfigList(params?: PayConfigListParams): Promise<ManageListResponse<PayConfigInfo>> {
|
||||
return http.get(http.adornUrl('/manage/payconfig/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 支付配置详情 */
|
||||
export function getPayConfigInfo(id: number): Promise<ManageResponse<{ config?: PayConfigInfo }>> {
|
||||
return http.get(http.adornUrl(`/manage/payconfig/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 新增/修改支付配置 */
|
||||
export function saveOrUpdatePayConfig(data: PayConfigSaveData): Promise<ManageResponse> {
|
||||
const url = data.id ? '/manage/payconfig/update' : '/manage/payconfig/save'
|
||||
return http.post(http.adornUrl(url), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 删除支付配置(批量软删除) */
|
||||
export function deletePayConfig(ids: number[]): Promise<ManageResponse> {
|
||||
return http.post(http.adornUrl('/manage/payconfig/delete'), http.adornData(ids, false)).then(res => res.data)
|
||||
}
|
||||
71
src/api/manage/price.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageListResponse, ManageResponse, ManagePageParams } from './types'
|
||||
|
||||
/**
|
||||
* 定价策略信息
|
||||
*/
|
||||
export interface PriceConfigInfo {
|
||||
id: number
|
||||
partnerId: number
|
||||
channelOrgId: number | null
|
||||
storeOrgId: number | null
|
||||
price: number
|
||||
durationHours: number
|
||||
name: string
|
||||
status: string
|
||||
/** 生效起始日期 yyyy-MM-dd */
|
||||
effectiveFrom: string | null
|
||||
/** 生效截止日期 yyyy-MM-dd(null=长期) */
|
||||
effectiveTo: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 定价策略列表查询参数
|
||||
*/
|
||||
export interface PriceListParams extends ManagePageParams {
|
||||
partnerId?: number
|
||||
channelOrgId?: number
|
||||
storeOrgId?: number
|
||||
status?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 定价策略保存/更新数据
|
||||
*/
|
||||
export interface PriceSaveData {
|
||||
id?: number
|
||||
partnerId?: number
|
||||
channelOrgId?: number
|
||||
storeOrgId?: number
|
||||
price: number
|
||||
durationHours?: number
|
||||
name?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
/** 定价策略分页列表 */
|
||||
export function getPriceList(params?: PriceListParams): Promise<ManageListResponse<PriceConfigInfo>> {
|
||||
return http.get(http.adornUrl('/manage/price/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 定价策略详情 */
|
||||
export function getPriceInfo(id: number): Promise<ManageResponse<{ config?: PriceConfigInfo }>> {
|
||||
return http.get(http.adornUrl(`/manage/price/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 新增/修改定价策略 */
|
||||
export function saveOrUpdatePrice(data: PriceSaveData): Promise<ManageResponse> {
|
||||
const url = data.id ? '/manage/price/update' : '/manage/price/save'
|
||||
return http.post(http.adornUrl(url), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 删除定价策略(批量软删除) */
|
||||
export function deletePrice(ids: number[]): Promise<ManageResponse> {
|
||||
return http.post(http.adornUrl('/manage/price/delete'), http.adornData(ids, false)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 查询生效中的定价(GET /manage/price/effective?channelOrgId=&storeOrgId=) */
|
||||
export function getEffectivePrice(params: { channelOrgId?: number; storeOrgId?: number }): Promise<ManageResponse<{ config?: PriceConfigInfo }>> {
|
||||
return http.get(http.adornUrl('/manage/price/effective'), { params: http.adornParams(params) }).then(res => res.data)
|
||||
}
|
||||
58
src/api/manage/profitReceiver.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageListResponse, ManageResponse, ManagePageParams } from './types'
|
||||
|
||||
// ===================== 分账接收方 =====================
|
||||
|
||||
export interface ProfitReceiver {
|
||||
id: number
|
||||
/** 关联组织ID(渠道/代理商/门店) */
|
||||
orgId: number
|
||||
orgName: string
|
||||
/** 接收方类型: MERCHANT_ID-商户号 / PERSONAL_OPENID-个人openid */
|
||||
receiverType: 'MERCHANT_ID' | 'PERSONAL_OPENID'
|
||||
/** 接收方账号(商户号或openid) */
|
||||
receiverAccount: string
|
||||
/** 收款方名称 */
|
||||
receiverName: string
|
||||
/** 绑定状态: PENDING-待确认 / ACTIVE-已绑定 / FAILED-绑定失败 */
|
||||
bindStatus: 'PENDING' | 'ACTIVE' | 'FAILED'
|
||||
}
|
||||
|
||||
export interface ProfitReceiverSaveBody {
|
||||
id?: number
|
||||
orgId: number
|
||||
receiverType: 'MERCHANT_ID' | 'PERSONAL_OPENID'
|
||||
receiverAccount: string
|
||||
receiverName: string
|
||||
}
|
||||
|
||||
export interface ProfitReceiverListParams extends ManagePageParams {
|
||||
orgId?: number
|
||||
bindStatus?: string
|
||||
}
|
||||
|
||||
/** 接收方列表 */
|
||||
export function getProfitReceiverList (params?: ProfitReceiverListParams): Promise<ManageListResponse<ProfitReceiver>> {
|
||||
return http.get(http.adornUrl('/manage/profit-receiver/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 接收方详情 */
|
||||
export function getProfitReceiverInfo (id: number): Promise<ManageResponse<{ receiver?: ProfitReceiver }>> {
|
||||
return http.get(http.adornUrl(`/manage/profit-receiver/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 保存接收方 */
|
||||
export function saveProfitReceiver (data: ProfitReceiverSaveBody): Promise<ManageResponse> {
|
||||
return http.post(http.adornUrl('/manage/profit-receiver/save'), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 删除接收方(body: Long[] ids) */
|
||||
export function deleteProfitReceiver (ids: number | number[]): Promise<ManageResponse> {
|
||||
const arr = Array.isArray(ids) ? ids : [ids]
|
||||
return http.post(http.adornUrl('/manage/profit-receiver/delete'), http.adornData(arr, false)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 同步接收方到微信(POST /manage/profit-receiver/sync/{id}) */
|
||||
export function syncProfitReceiver (id: number): Promise<ManageResponse> {
|
||||
return http.post(http.adornUrl(`/manage/profit-receiver/sync/${id}`), http.adornData({})).then(res => res.data)
|
||||
}
|
||||
100
src/api/manage/profitRule.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageListResponse, ManageResponse, ManagePageParams } from './types'
|
||||
|
||||
// ===================== 分润规则 =====================
|
||||
|
||||
export interface ProfitRule {
|
||||
id: number
|
||||
ruleName: string
|
||||
scopeType: 'GLOBAL' | 'CHANNEL' | 'AGENT' | 'STORE'
|
||||
scopeOrgId: number | null
|
||||
scopeOrgName: string | null
|
||||
priority: number
|
||||
status: 'ACTIVE' | 'INACTIVE'
|
||||
effectiveFrom: string | null
|
||||
effectiveTo: string | null
|
||||
}
|
||||
|
||||
export interface ProfitRuleItem {
|
||||
id: number
|
||||
orgRole: 'PLATFORM' | 'CHANNEL' | 'AGENT' | 'STORE'
|
||||
shareType: 'RATIO' | 'FIXED'
|
||||
/** RATIO: 百分比(如 20 表示 20%);FIXED: 分 */
|
||||
shareValue: number
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
export interface ProfitRuleDetail extends ProfitRule {
|
||||
items: ProfitRuleItem[]
|
||||
}
|
||||
|
||||
export interface ProfitRuleSaveBody {
|
||||
id?: number
|
||||
ruleName: string
|
||||
scopeType: 'GLOBAL' | 'CHANNEL' | 'AGENT' | 'STORE'
|
||||
scopeOrgId: number | null
|
||||
priority: number
|
||||
status: 'ACTIVE' | 'INACTIVE'
|
||||
effectiveFrom: string | null
|
||||
effectiveTo: string | null
|
||||
items: Omit<ProfitRuleItem, 'id'>[]
|
||||
}
|
||||
|
||||
export interface ProfitRuleListParams extends ManagePageParams {
|
||||
scopeType?: string
|
||||
}
|
||||
|
||||
// ===================== 试算 =====================
|
||||
|
||||
export interface ProfitRulePreviewBody {
|
||||
storeOrgId: number
|
||||
/** 单位:分 */
|
||||
amount: number
|
||||
}
|
||||
|
||||
export interface ProfitRulePreviewDetail {
|
||||
orgRole: 'PLATFORM' | 'CHANNEL' | 'AGENT' | 'STORE'
|
||||
orgId: number
|
||||
orgName: string
|
||||
shareType: 'RATIO' | 'FIXED'
|
||||
shareValue: number
|
||||
/** 计算后金额(分) */
|
||||
amount: number
|
||||
}
|
||||
|
||||
export interface ProfitRulePreviewResp {
|
||||
ruleId: number
|
||||
ruleName: string
|
||||
details: ProfitRulePreviewDetail[]
|
||||
nonPlatformTotalRatio: number
|
||||
warning: string | null
|
||||
}
|
||||
|
||||
// ===================== API 方法 =====================
|
||||
|
||||
/** 分润规则列表 */
|
||||
export function getProfitRuleList (params?: ProfitRuleListParams): Promise<ManageListResponse<ProfitRule>> {
|
||||
return http.get(http.adornUrl('/manage/profit-rule/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 分润规则详情(含 items 子表) */
|
||||
export function getProfitRuleInfo (id: number): Promise<ManageResponse<{ rule?: ProfitRuleDetail }>> {
|
||||
return http.get(http.adornUrl(`/manage/profit-rule/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 保存/更新分润规则 */
|
||||
export function saveOrUpdateProfitRule (data: ProfitRuleSaveBody): Promise<ManageResponse> {
|
||||
const url = data.id ? '/manage/profit-rule/update' : '/manage/profit-rule/save'
|
||||
return http.post(http.adornUrl(url), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 删除分润规则(body: Long[] ids) */
|
||||
export function deleteProfitRule (ids: number | number[]): Promise<ManageResponse> {
|
||||
const arr = Array.isArray(ids) ? ids : [ids]
|
||||
return http.post(http.adornUrl('/manage/profit-rule/delete'), http.adornData(arr, false)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 试算分润(POST /manage/profit-rule/preview) */
|
||||
export function previewProfitRule (data: ProfitRulePreviewBody): Promise<ManageResponse<ProfitRulePreviewResp>> {
|
||||
return http.post(http.adornUrl('/manage/profit-rule/preview'), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
86
src/api/manage/profitSharing.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageListResponse, ManageResponse, ManagePageParams } from './types'
|
||||
|
||||
// ===================== 分账记录 =====================
|
||||
|
||||
export interface ProfitSharingItem {
|
||||
id: number
|
||||
/** 支付订单号 */
|
||||
payOrderNo: string
|
||||
/** 分账单号 */
|
||||
outOrderNo: string
|
||||
status: 'PENDING' | 'SUCCESS' | 'FAILED' | 'PARTIAL'
|
||||
/** 订单总金额(分) */
|
||||
totalAmount: number
|
||||
ruleName: string | null
|
||||
createdAt: string
|
||||
storeName: string | null
|
||||
channelName: string | null
|
||||
}
|
||||
|
||||
export interface ProfitSharingDetailItem {
|
||||
id: number
|
||||
orgRole: 'PLATFORM' | 'CHANNEL' | 'AGENT' | 'STORE'
|
||||
orgName: string | null
|
||||
receiverAccount: string
|
||||
/** 分账金额(分) */
|
||||
amount: number
|
||||
status: 'PENDING' | 'SUCCESS' | 'FAILED'
|
||||
successTime: string | null
|
||||
failReason: string | null
|
||||
}
|
||||
|
||||
export interface ProfitSharingDetail extends ProfitSharingItem {
|
||||
/** 规则快照 JSON */
|
||||
ruleSnapshot: string | null
|
||||
/** 组织链路快照 */
|
||||
orgPathSnapshot: string | null
|
||||
details: ProfitSharingDetailItem[]
|
||||
}
|
||||
|
||||
export interface ProfitSharingListParams extends ManagePageParams {
|
||||
orderNo?: string
|
||||
status?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
/** 分账记录列表(含 details 子表) */
|
||||
export function getProfitSharingList (params?: ProfitSharingListParams): Promise<ManageListResponse<ProfitSharingItem>> {
|
||||
return http.get(http.adornUrl('/manage/profit-sharing/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 分账记录详情(含明细) */
|
||||
export function getProfitSharingInfo (id: number): Promise<ManageResponse<{ sharing?: ProfitSharingDetail }>> {
|
||||
return http.get(http.adornUrl(`/manage/profit-sharing/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 分账统计 */
|
||||
export interface ProfitSharingStatsSummary {
|
||||
totalCount: number
|
||||
totalAmount: number
|
||||
successCount: number
|
||||
successAmount: number
|
||||
}
|
||||
|
||||
export interface ProfitSharingStatsGroup {
|
||||
orgId: number
|
||||
orgName: string
|
||||
orgType: string
|
||||
count: number
|
||||
amount: number
|
||||
}
|
||||
|
||||
export interface ProfitSharingStats {
|
||||
summary: ProfitSharingStatsSummary
|
||||
groups: ProfitSharingStatsGroup[]
|
||||
}
|
||||
|
||||
export function getProfitSharingStats (params?: { startDate?: string; endDate?: string; groupBy?: string }): Promise<ManageResponse<{ stats?: ProfitSharingStats }>> {
|
||||
return http.get(http.adornUrl('/manage/profit-sharing/stats'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 重试分账 */
|
||||
export function retryProfitSharing (id: number): Promise<ManageResponse> {
|
||||
return http.post(http.adornUrl('/manage/profit-sharing/retry'), http.adornData({ id })).then(res => res.data)
|
||||
}
|
||||
60
src/api/manage/refund.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageListResponse, ManageResponse, ManagePageParams } from './types'
|
||||
|
||||
// ===================== 退款记录 =====================
|
||||
|
||||
export interface RefundItem {
|
||||
id: number
|
||||
/** 退款单号 */
|
||||
refundNo: string
|
||||
/** 原支付订单号 */
|
||||
payOrderNo: string
|
||||
/** 退款金额(分) */
|
||||
refundAmount: number
|
||||
/** 原订单金额(分) */
|
||||
totalAmount: number
|
||||
/** 退款状态 */
|
||||
status: 'PENDING' | 'PROCESSING' | 'SUCCESS' | 'FAILED' | 'CLOSED'
|
||||
/** 退款原因 */
|
||||
reason: string | null
|
||||
/** 微信退款单号 */
|
||||
wxRefundId: string | null
|
||||
/** 退款发起人 */
|
||||
operator: string | null
|
||||
errorMessage: string | null
|
||||
refundedAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface RefundListParams extends ManagePageParams {
|
||||
refundNo?: string
|
||||
payOrderNo?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
/** 发起退款请求体(文档:body {orderNo, reason}) */
|
||||
export interface RefundApplyBody {
|
||||
orderNo: string
|
||||
reason: string
|
||||
}
|
||||
|
||||
/** 退款记录列表 */
|
||||
export function getRefundList (params?: RefundListParams): Promise<ManageListResponse<RefundItem>> {
|
||||
return http.get(http.adornUrl('/manage/refund/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 退款详情(按退款单号 refundNo 查询) */
|
||||
export function getRefundInfo (refundNo: string): Promise<ManageResponse<{ refund?: RefundItem }>> {
|
||||
return http.get(http.adornUrl(`/manage/refund/info/${refundNo}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 发起退款(POST /manage/refund/apply) */
|
||||
export function applyRefund (data: RefundApplyBody): Promise<ManageResponse> {
|
||||
return http.post(http.adornUrl('/manage/refund/apply'), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 重试退款 */
|
||||
export function retryRefund (id: number): Promise<ManageResponse> {
|
||||
return http.post(http.adornUrl('/manage/refund/retry'), http.adornData({ id })).then(res => res.data)
|
||||
}
|
||||
73
src/api/manage/stats.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageResponse } from './types'
|
||||
|
||||
/**
|
||||
* 总览看板统计
|
||||
*/
|
||||
export interface OverviewStats {
|
||||
totalOrders: number
|
||||
todayOrders: number
|
||||
totalRevenue: number
|
||||
todayRevenue: number
|
||||
totalPool: number
|
||||
availablePool: number
|
||||
usedPool: number
|
||||
activateFailed: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单趋势统计项
|
||||
*/
|
||||
export interface OrderTrendItem {
|
||||
date: string
|
||||
orderCount: number
|
||||
revenue: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 库存统计
|
||||
* 实际返回:statusStats / activateStats 均为 Map<状态, 数量>
|
||||
*/
|
||||
export interface PoolStats {
|
||||
/** 按库存状态:{ UNUSED: 1, USED: 2 } */
|
||||
statusStats: Record<string, number>
|
||||
/** 按激活状态:{ PENDING: 2, FAILED: 1 } */
|
||||
activateStats: Record<string, number>
|
||||
}
|
||||
|
||||
/**
|
||||
* 渠道统计项
|
||||
*/
|
||||
export interface ChannelStatItem {
|
||||
channelOrgId: number
|
||||
orderCount: number
|
||||
revenue: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单趋势统计查询参数
|
||||
*/
|
||||
export interface OrderStatsParams {
|
||||
startDate: string
|
||||
endDate: string
|
||||
}
|
||||
|
||||
/** 总览看板 */
|
||||
export function getOverviewStats(): Promise<ManageResponse<{ overview?: OverviewStats }>> {
|
||||
return http.get(http.adornUrl('/manage/stats/overview'), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 订单趋势统计 */
|
||||
export function getOrderStats(params: OrderStatsParams): Promise<ManageResponse<{ orderStats?: { list: OrderTrendItem[] } }>> {
|
||||
return http.get(http.adornUrl('/manage/stats/orders'), { params: http.adornParams(params) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 库存统计 */
|
||||
export function getPoolStats(): Promise<ManageResponse<{ poolStats?: PoolStats }>> {
|
||||
return http.get(http.adornUrl('/manage/stats/pool'), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 渠道统计 */
|
||||
export function getChannelStats(): Promise<ManageResponse<{ channelStats?: { list: ChannelStatItem[] } }>> {
|
||||
return http.get(http.adornUrl('/manage/stats/channels'), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
63
src/api/manage/store.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ManageListResponse, ManageResponse, ManagePageParams } from './types'
|
||||
|
||||
/**
|
||||
* 门店信息
|
||||
*/
|
||||
export interface StoreInfo {
|
||||
id: number
|
||||
parentId: number
|
||||
mpath: string
|
||||
name: string
|
||||
orderNo?: number
|
||||
type: string
|
||||
/** 后端 @JsonProperty("orgCode") 重命名,JSON 中为 orgCode */
|
||||
orgCode?: string
|
||||
status: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
/** 非表字段,后端 join 返回 */
|
||||
parentOrgName?: string
|
||||
channelName?: string
|
||||
children?: StoreInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 门店列表查询参数
|
||||
*/
|
||||
export interface StoreListParams extends ManagePageParams {
|
||||
name?: string
|
||||
status?: string
|
||||
channelOrgId?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 门店保存/更新数据
|
||||
*/
|
||||
export interface StoreSaveData {
|
||||
id?: number
|
||||
parentId: number
|
||||
name: string
|
||||
orgCode?: string
|
||||
}
|
||||
|
||||
/** 门店分页列表 */
|
||||
export function getStoreList(params?: StoreListParams): Promise<ManageListResponse<StoreInfo>> {
|
||||
return http.get(http.adornUrl('/manage/org/store/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 门店详情 */
|
||||
export function getStoreInfo(id: number): Promise<ManageResponse<{ store?: StoreInfo }>> {
|
||||
return http.get(http.adornUrl(`/manage/org/store/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 新增/修改门店 */
|
||||
export function saveOrUpdateStore(data: StoreSaveData): Promise<ManageResponse> {
|
||||
const url = data.id ? '/manage/org/store/update' : '/manage/org/store/save'
|
||||
return http.post(http.adornUrl(url), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 删除门店(批量软删除) */
|
||||
export function deleteStore(ids: number[]): Promise<ManageResponse> {
|
||||
return http.post(http.adornUrl('/manage/org/store/delete'), http.adornData(ids, false)).then(res => res.data)
|
||||
}
|
||||
33
src/api/manage/types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* /manage/ 接口共享类型定义
|
||||
*
|
||||
* 统一响应格式:{ code, msg, data: {...} }
|
||||
* - 业务数据统一在 data 字段内
|
||||
* - 分页结构在 data.page 内:{ data: { page: { totalCount, list, ... } } }
|
||||
*/
|
||||
|
||||
/** /manage/ 接口统一响应基础格式 */
|
||||
export interface ManageResponse<T = any> {
|
||||
code: number
|
||||
msg: string
|
||||
data: T
|
||||
}
|
||||
|
||||
/** /manage/ 分页 page 对象 */
|
||||
export interface ManagePage<T = any> {
|
||||
totalCount: number
|
||||
pageSize?: number
|
||||
totalPage?: number
|
||||
currPage?: number
|
||||
list: T[]
|
||||
}
|
||||
|
||||
/** /manage/ 分页列表响应 */
|
||||
export interface ManageListResponse<T = any> extends ManageResponse<{ page: ManagePage<T> }> {}
|
||||
|
||||
/** 分页查询参数 */
|
||||
export interface ManagePageParams {
|
||||
page?: number
|
||||
limit?: number
|
||||
[key: string]: any
|
||||
}
|
||||
53
src/api/mediaAsset.ts
Executable file
@@ -0,0 +1,53 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ApiResponse, PageParams, PageResult } from '@/types/api'
|
||||
|
||||
/**
|
||||
* 媒资信息
|
||||
*/
|
||||
export interface MediaAsset {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
isOnline?: number
|
||||
deleteFlag?: number
|
||||
region?: string
|
||||
releaseYear?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 媒资列表参数
|
||||
*/
|
||||
export interface MediaAssetListParams extends PageParams {
|
||||
name?: string
|
||||
code?: string
|
||||
isOnline?: number
|
||||
deleteFlag?: number
|
||||
region?: string
|
||||
releaseYear?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 媒资分页列表
|
||||
*/
|
||||
export function getMediaAssetList(
|
||||
params?: MediaAssetListParams
|
||||
): Promise<ApiResponse<PageResult<MediaAsset>>> {
|
||||
return http.get(http.adornUrl('/sys/mediaAsset/list'), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
|
||||
/** 媒资详情 */
|
||||
export function getMediaAssetInfo(id: number): Promise<ApiResponse<{ mediaAsset: MediaAsset }>> {
|
||||
return http.get(http.adornUrl(`/sys/mediaAsset/info/${id}`), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 修改/添加媒资 */
|
||||
export function saveMediaAsset(data: Partial<MediaAsset>): Promise<ApiResponse<any>> {
|
||||
const url = data.id ? '/sys/mediaAsset/update' : '/sys/mediaAsset/save'
|
||||
return http.post(http.adornUrl(url), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 删除媒资 */
|
||||
export function deleteMediaAsset(ids: number[]): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/mediaAsset/delete'), http.adornData(ids, false))
|
||||
}
|
||||
95
src/api/mediaAssetRank.ts
Executable file
@@ -0,0 +1,95 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
|
||||
/**
|
||||
* 媒资排行项
|
||||
*/
|
||||
export interface MediaAssetRankItem {
|
||||
code: string
|
||||
name: string
|
||||
sortOrder: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置排行榜数据
|
||||
*/
|
||||
export interface SetRankData {
|
||||
codeList: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序更新数据
|
||||
*/
|
||||
export interface UpdateSortData {
|
||||
code?: string
|
||||
targetSortOrder?: number
|
||||
orderList?: Array<{ code: string; sortOrder: number }>
|
||||
}
|
||||
|
||||
/** 获取当前 Top10 榜(含媒资信息) */
|
||||
export function getMediaAssetRankTop10(): Promise<ApiResponse<{ rankList: MediaAssetRankItem[] }>> {
|
||||
return http.get(http.adornUrl('/sys/mediaAssetRank/top10'), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 当前排序/序号信息 */
|
||||
export function getMediaAssetRankGetSortOrder(): Promise<ApiResponse<any>> {
|
||||
return http.get(http.adornUrl('/sys/mediaAssetRank/getSortOrder'), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置排行榜
|
||||
* 若依侧通过 adornData 会附带 t 等默认字段,一般与 Jackson 兼容。
|
||||
* @param data 仅编码数组,或 { codeList }
|
||||
*/
|
||||
export function setMediaAssetRank(data: string[] | SetRankData): Promise<ApiResponse<any>> {
|
||||
const codeList = Array.isArray(data)
|
||||
? data
|
||||
: data?.codeList || []
|
||||
return http.post(
|
||||
http.adornUrl('/sys/mediaAssetRank/set'),
|
||||
http.adornData({ codeList }, true)
|
||||
)
|
||||
}
|
||||
|
||||
/** 单条移除 */
|
||||
export function removeMediaAssetRank(code: string): Promise<ApiResponse<any>> {
|
||||
return http.post(
|
||||
http.adornUrl('/sys/mediaAssetRank/remove'),
|
||||
http.adornData({ code }, true)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整排序
|
||||
* 请求体与后端一致,如 { code, targetSortOrder } 或 { orderList: [...] } 等
|
||||
*/
|
||||
export function updateMediaAssetRankSort(data: UpdateSortData): Promise<ApiResponse<any>> {
|
||||
return http.post(
|
||||
http.adornUrl('/sys/mediaAssetRank/updateSort'),
|
||||
http.adornData(data && typeof data === 'object' ? data : {}, true)
|
||||
)
|
||||
}
|
||||
|
||||
/** 上移一位 */
|
||||
export function moveUpMediaAssetRank(code: string): Promise<ApiResponse<any>> {
|
||||
return http.post(
|
||||
http.adornUrl('/sys/mediaAssetRank/moveUp'),
|
||||
http.adornData({}),
|
||||
{ params: http.adornParams({ code }) }
|
||||
)
|
||||
}
|
||||
|
||||
/** 下移一位 */
|
||||
export function moveDownMediaAssetRank(code: string): Promise<ApiResponse<any>> {
|
||||
return http.post(
|
||||
http.adornUrl('/sys/mediaAssetRank/moveDown'),
|
||||
http.adornData({}),
|
||||
{ params: http.adornParams({ code }) }
|
||||
)
|
||||
}
|
||||
|
||||
/** 清空排行榜 */
|
||||
export function clearMediaAssetRank(): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/mediaAssetRank/clear'), http.adornData({}))
|
||||
}
|
||||
148
src/api/org.ts
Executable file
@@ -0,0 +1,148 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ApiResponse, PageParams, PageResult } from '@/types/api'
|
||||
|
||||
/**
|
||||
* 组织信息
|
||||
*/
|
||||
export interface OrgInfo {
|
||||
id: number
|
||||
name: string
|
||||
parentId?: number
|
||||
type?: string
|
||||
status?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 组织列表参数
|
||||
*/
|
||||
export interface OrgListParams extends PageParams {
|
||||
name?: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 组织卡统计
|
||||
*/
|
||||
export interface OrgCardStats {
|
||||
allocatable: number
|
||||
activated: number
|
||||
pendingActivation: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 组织卡总览
|
||||
*/
|
||||
export interface OrgCardOverview {
|
||||
totalCount: number
|
||||
usedCount: number
|
||||
availableCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 组织版本信息
|
||||
*/
|
||||
export interface OrgVersionInfo {
|
||||
orgId: number
|
||||
verCode?: string
|
||||
verName?: string
|
||||
url?: string
|
||||
touchUrl?: string
|
||||
keyUrl?: string
|
||||
compulsory?: boolean
|
||||
}
|
||||
|
||||
/** 组织树 */
|
||||
export function getOrgTree(params?: OrgListParams): Promise<ApiResponse<OrgInfo[]>> {
|
||||
return http.get(http.adornUrl('/manage/org/tree'), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
|
||||
|
||||
/** 组织树(含门店):全类型组织树,含 STORE 节点,用于门店选择器等 */
|
||||
export function getOrgAllTree(params?: OrgListParams): Promise<ApiResponse<OrgInfo[]>> {
|
||||
return http.get(http.adornUrl('/manage/org/all-tree'), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 /manage/org/list-data 等接口响应体中的扁平组织列表。
|
||||
* 兼容:顶层 `list`、或 `data` 为数组、或 `data: { list }`。
|
||||
*/
|
||||
export function parseOrgListDataBody(body: Record<string, any> | null | undefined): OrgInfo[] {
|
||||
if (!body) return []
|
||||
if (Array.isArray(body.list)) return body.list as OrgInfo[]
|
||||
const inner = body.data
|
||||
if (Array.isArray(inner)) return inner as OrgInfo[]
|
||||
if (inner && typeof inner === 'object' && Array.isArray(inner.list)) return inner.list as OrgInfo[]
|
||||
return []
|
||||
}
|
||||
|
||||
/** 组织列表(扁平,用于下拉/树选) */
|
||||
export function getOrgListData(params?: OrgListParams): Promise<ApiResponse<OrgInfo[]>> {
|
||||
return http.get(http.adornUrl('/manage/org/list-data'), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
|
||||
/** 组织列表(/list) */
|
||||
export function getOrgList(params?: OrgListParams): Promise<ApiResponse<PageResult<OrgInfo>>> {
|
||||
return http.get(http.adornUrl('/manage/org/list'), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
|
||||
/** 当前账号数据范围内全部渠道(type=CHANNEL、ACTIVE)扁平列表 */
|
||||
export function getOrgChannels(): Promise<ApiResponse<OrgInfo[]>> {
|
||||
return http.get(http.adornUrl('/manage/org/channels'), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 指定渠道下仅 SERVICE_PROVIDER 的多级树(不含门店) */
|
||||
export function getChannelAgentTree(channelId: number): Promise<ApiResponse<OrgInfo[]>> {
|
||||
return http.get(http.adornUrl(`/manage/org/channel/${channelId}/agent-tree`), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 组织详情 */
|
||||
export function getOrgInfo(id: number): Promise<ApiResponse<OrgInfo>> {
|
||||
return http.get(http.adornUrl(`/manage/org/info/${id}`), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 删除组织(单个或批量) */
|
||||
export function deleteOrg(ids: number[]): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/manage/org/delete'), http.adornData(ids, false))
|
||||
}
|
||||
|
||||
/** 组织下卡统计(可分配/已激活/待激活) */
|
||||
export function getOrgCardStats(orgId: number): Promise<ApiResponse<OrgCardStats>> {
|
||||
return http.get(http.adornUrl(`/manage/org/${orgId}/card-stats`), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 组织卡总览统计(用于设备导入页顶部数据概览) */
|
||||
export function getOrgCardOverview(params?: { orgId?: number }): Promise<ApiResponse<OrgCardOverview>> {
|
||||
return http.get(http.adornUrl('/manage/org/card-overview'), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
|
||||
/** 按卡类型维度的明细统计(首页/全库口径) */
|
||||
export function getOrgCardTypeDetailStats(params?: { orgId?: number }): Promise<ApiResponse<any>> {
|
||||
return http.get(http.adornUrl('/manage/org/card-type-detail-stats'), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
|
||||
/** 同渠道组织树(用于设备卡转移选择目标组织) */
|
||||
export function getSameChannelOrgs(orgId: number): Promise<ApiResponse<OrgInfo[]>> {
|
||||
return http.get(http.adornUrl(`/manage/org/${orgId}/same-channel-orgs`), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 组织保存/更新(根据是否有 id 决定) */
|
||||
export function saveOrUpdateOrg(data: Partial<OrgInfo>): Promise<ApiResponse<any>> {
|
||||
const url = data.id ? '/manage/org/update' : '/manage/org/save'
|
||||
return http.post(http.adornUrl(url), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 组织版本详情 */
|
||||
export function getOrgVersionInfo(orgId: number): Promise<ApiResponse<OrgVersionInfo>> {
|
||||
return http.get(http.adornUrl('/sys/org-version/info'), { params: http.adornParams({ orgId }) })
|
||||
}
|
||||
|
||||
/** 组织版本保存 { orgId, verCode, ver_name, url, compulsory } */
|
||||
export function saveOrgVersion(data: OrgVersionInfo): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/org-version/save'), http.adornData(data))
|
||||
}
|
||||
|
||||
/** 删除组织版本 */
|
||||
export function deleteOrgVersion(orgId: number): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/org-version/delete'), http.adornData({ orgId }))
|
||||
}
|
||||
60
src/api/oss.ts
Executable file
@@ -0,0 +1,60 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
/**
|
||||
* 后端 STS 临时凭证(字段名兼容常见驼峰写法;与 COS getAuthorization 入参对齐)
|
||||
*/
|
||||
export interface OssStsCredentialPayload {
|
||||
TmpSecretId?: string
|
||||
tmpSecretId?: string
|
||||
TmpSecretKey?: string
|
||||
tmpSecretKey?: string
|
||||
SecurityToken?: string
|
||||
securityToken?: string
|
||||
sessionToken?: string
|
||||
/** 过期时间戳(秒,或毫秒则由上传工具归一化) */
|
||||
ExpiredTime?: number
|
||||
expiredTime?: number
|
||||
bucket?: string
|
||||
Bucket?: string
|
||||
region?: string
|
||||
Region?: string
|
||||
/** 对象 Key(须与 STS 策略一致,不可自行改写) */
|
||||
key?: string
|
||||
Key?: string
|
||||
/** 上传成功后的访问地址 */
|
||||
url?: string
|
||||
/** 以下字段不参与 COS 前端上传,仅占位兼容接口 */
|
||||
appId?: number
|
||||
startTime?: number
|
||||
durationSeconds?: number
|
||||
}
|
||||
|
||||
/** GET /sys/oss/sts-credential 实际响应:凭证在顶层 `credential`(非标准 `data`) */
|
||||
export interface OssStsCredentialApiResponse {
|
||||
code: number
|
||||
msg: string
|
||||
credential?: OssStsCredentialPayload
|
||||
/** 兼容若后端改为包在 data 内 */
|
||||
data?: OssStsCredentialPayload
|
||||
}
|
||||
|
||||
export interface OssStsCredentialOptions {
|
||||
/** 为 true 时请求保留原始文件名(不去重哈希),需后端 sts-credential 支持 */
|
||||
keepOriginalFileName?: boolean
|
||||
}
|
||||
|
||||
/** GET /sys/oss/sts-credential?filename=xxx */
|
||||
export function getOssStsCredential (
|
||||
filename: string,
|
||||
options?: OssStsCredentialOptions
|
||||
): Promise<OssStsCredentialApiResponse> {
|
||||
const params: Record<string, string | number> = { filename }
|
||||
if (options?.keepOriginalFileName) {
|
||||
params.keepOriginalFileName = 1
|
||||
params.dedupe = 0
|
||||
}
|
||||
return http
|
||||
.get(http.adornUrl('/sys/oss/sts-credential'), {
|
||||
params: http.adornParams(params),
|
||||
})
|
||||
.then(res => res.data)
|
||||
}
|
||||
54
src/api/store.ts
Executable file
@@ -0,0 +1,54 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ApiResponse, PageParams, PageResult } from '@/types/api'
|
||||
|
||||
/**
|
||||
* 门店信息
|
||||
*/
|
||||
export interface StoreInfo {
|
||||
id: number
|
||||
name: string
|
||||
orgId: number
|
||||
status?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 门店列表参数
|
||||
*/
|
||||
export interface StoreListParams extends PageParams {
|
||||
name?: string
|
||||
orgId?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 门店卡信息
|
||||
*/
|
||||
export interface StoreCard {
|
||||
id: number
|
||||
cardNo: string
|
||||
status: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/** 门店列表(下拉等场景) */
|
||||
export function getStoreList(params?: StoreListParams): Promise<ApiResponse<StoreInfo[]>> {
|
||||
return http.get(http.adornUrl('/manage/store/list'), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
|
||||
/** 按代理商组织查询门店(与 list + orgId=代理商 等价;会校验目标为 SERVICE_PROVIDER) */
|
||||
export function getStoreListByAgent(agentOrgId: number): Promise<ApiResponse<StoreInfo[]>> {
|
||||
return http.get(http.adornUrl(`/manage/store/list-by-agent/${agentOrgId}`), { params: http.adornParams() })
|
||||
}
|
||||
|
||||
/** 门店搜索(分页) */
|
||||
export function searchStore(params?: StoreListParams): Promise<ApiResponse<PageResult<StoreInfo>>> {
|
||||
return http.get(http.adornUrl('/manage/store/search'), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
|
||||
/** 门店下的卡列表 */
|
||||
export function getStoreCards(
|
||||
storeId: number,
|
||||
params?: PageParams
|
||||
): Promise<ApiResponse<PageResult<StoreCard>>> {
|
||||
return http.get(http.adornUrl(`/manage/store/${storeId}/cards`), { params: http.adornParams(params || {}) })
|
||||
}
|
||||
351
src/api/sys.ts
Executable file
@@ -0,0 +1,351 @@
|
||||
import http from '@/utils/httpRequest'
|
||||
import type { ApiResponse, PageParams, PageResult } from '@/types/api'
|
||||
import { uploadFileViaStsCos } from '@/utils/ossCosUpload'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/**
|
||||
* 登录表单数据
|
||||
*/
|
||||
export interface LoginData {
|
||||
username: string
|
||||
password: string
|
||||
captcha: string
|
||||
uuid: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录响应
|
||||
*/
|
||||
export interface LoginResult {
|
||||
token: string
|
||||
expire: number
|
||||
refreshToken?: string
|
||||
refreshExpire?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
export interface UserInfo {
|
||||
id: number
|
||||
username: string
|
||||
realName?: string
|
||||
avatar?: string
|
||||
roleIdList?: number[]
|
||||
orgId?: number
|
||||
orgName?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单信息
|
||||
*/
|
||||
export interface MenuInfo {
|
||||
id: number
|
||||
parentId?: number
|
||||
name: string
|
||||
url?: string
|
||||
icon?: string
|
||||
sort?: number
|
||||
show?: number
|
||||
perms?: string
|
||||
list?: MenuInfo[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单导航响应
|
||||
*/
|
||||
export interface MenuNavResponse {
|
||||
code: number
|
||||
menuList: MenuInfo[]
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色信息
|
||||
*/
|
||||
export interface RoleInfo {
|
||||
id: number
|
||||
roleName: string
|
||||
remark?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数配置
|
||||
*/
|
||||
export interface ConfigInfo {
|
||||
id: number
|
||||
paramKey: string
|
||||
paramValue: string
|
||||
remark?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时任务
|
||||
*/
|
||||
export interface ScheduleInfo {
|
||||
jobId?: number
|
||||
beanName: string
|
||||
methodName: string
|
||||
params?: string
|
||||
cronExpression: string
|
||||
status?: number
|
||||
remark?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志信息
|
||||
*/
|
||||
export interface LogInfo {
|
||||
id: number
|
||||
operation: string
|
||||
method: string
|
||||
params?: string
|
||||
time?: number
|
||||
ip?: string
|
||||
createDate?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 审计日志
|
||||
*/
|
||||
export interface AuditLogInfo extends LogInfo {
|
||||
module?: string
|
||||
action?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/** 吉客云 / JST API 调用日志(设备与组织可为空) */
|
||||
export interface ApiJstCallLogInfo {
|
||||
id?: number
|
||||
apiPath?: string | null
|
||||
requestBody?: string | null
|
||||
dataId?: string | number | null
|
||||
createTime?: string | null
|
||||
mac?: string | null
|
||||
deviceSn?: string | null
|
||||
deviceUid?: string | null
|
||||
storeOrgId?: number | null
|
||||
storeOrgName?: string | null
|
||||
storeOrgCode?: string | null
|
||||
storeOrgType?: string | null
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时任务日志
|
||||
*/
|
||||
export interface ScheduleLogInfo {
|
||||
logId: number
|
||||
jobId: number
|
||||
beanName: string
|
||||
methodName: string
|
||||
params?: string
|
||||
status?: number
|
||||
error?: string
|
||||
times?: number
|
||||
createTime?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// ==================== 登录 / 认证 ====================
|
||||
|
||||
export function login(data: LoginData): Promise<ApiResponse<LoginResult>> {
|
||||
return http.post(http.adornUrl('/sys/login'), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
export function logout(): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/logout'), http.adornData({})).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 获取当前登录用户信息;若传入 userId,则获取指定用户信息 */
|
||||
export function getUserInfo(userId?: number | string | null): Promise<ApiResponse<UserInfo>> {
|
||||
const url = userId !== null && userId !== undefined && userId !== ''
|
||||
? `/manage/user/info/${userId}`
|
||||
: '/manage/user/info'
|
||||
return http.get(http.adornUrl(url), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
export function updatePassword(data: { password: string; newPassword: string }): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/manage/user/password'), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
// ==================== 菜单 ====================
|
||||
|
||||
export function getMenuNav(): Promise<ApiResponse<MenuNavResponse>> {
|
||||
return http.get(http.adornUrl('/manage/menu/nav'), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 验证码图片地址(img src 需完整路径 /api/xxx,走代理) */
|
||||
export function getCaptchaUrl(uuid: string): string {
|
||||
return http.adornUrl(`/captcha.jpg?uuid=${uuid}`)
|
||||
}
|
||||
|
||||
export function getMenuList(params?: PageParams): Promise<ApiResponse<PageResult<MenuInfo>>> {
|
||||
return http.get(http.adornUrl('/manage/menu/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
export function getMenuSelect(): Promise<ApiResponse<{ menuList: MenuInfo[] }>> {
|
||||
return http.get(http.adornUrl('/manage/menu/select'), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
export function getMenuInfo(id: number): Promise<ApiResponse<MenuInfo>> {
|
||||
return http.get(http.adornUrl(`/manage/menu/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
export function saveMenu(data: Partial<MenuInfo>): Promise<ApiResponse<any>> {
|
||||
const url = data.id ? '/manage/menu/update' : '/manage/menu/save'
|
||||
return http.post(http.adornUrl(url), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
export function deleteMenu(id: number): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl(`/manage/menu/delete/${id}`), http.adornData({})).then(res => res.data)
|
||||
}
|
||||
|
||||
// ==================== 用户 ====================
|
||||
|
||||
export function getUserList(params?: PageParams): Promise<ApiResponse<PageResult<UserInfo>>> {
|
||||
return http.get(http.adornUrl('/manage/user/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
export function getUserInfoById(id: number): Promise<ApiResponse<UserInfo>> {
|
||||
return getUserInfo(id)
|
||||
}
|
||||
|
||||
export function saveUser(data: Partial<UserInfo>): Promise<ApiResponse<any>> {
|
||||
const url = data.id ? '/manage/user/update' : '/manage/user/save'
|
||||
return http.post(http.adornUrl(url), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
export function deleteUser(ids: number[]): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/manage/user/delete'), http.adornData(ids, false)).then(res => res.data)
|
||||
}
|
||||
|
||||
export function getRoleSelect(): Promise<ApiResponse<{ list: RoleInfo[] }>> {
|
||||
return http.get(http.adornUrl('/manage/role/select'), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
// ==================== 角色 ====================
|
||||
|
||||
export function getRoleList(params?: PageParams): Promise<ApiResponse<PageResult<RoleInfo>>> {
|
||||
return http.get(http.adornUrl('/manage/role/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
export function getRoleInfo(id: number): Promise<ApiResponse<RoleInfo>> {
|
||||
return http.get(http.adornUrl(`/manage/role/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
export function saveRole(data: Partial<RoleInfo>): Promise<ApiResponse<any>> {
|
||||
const url = data.id ? '/manage/role/update' : '/manage/role/save'
|
||||
return http.post(http.adornUrl(url), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
export function deleteRole(ids: number[]): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/manage/role/delete'), http.adornData(ids, false)).then(res => res.data)
|
||||
}
|
||||
|
||||
// ==================== 参数配置 ====================
|
||||
|
||||
export function getConfigList(params?: PageParams): Promise<ApiResponse<PageResult<ConfigInfo>>> {
|
||||
return http.get(http.adornUrl('/sys/config/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
export function getConfigInfo(id: number): Promise<ApiResponse<ConfigInfo>> {
|
||||
return http.get(http.adornUrl(`/sys/config/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
export function saveConfig(data: Partial<ConfigInfo>): Promise<ApiResponse<any>> {
|
||||
const url = data.id ? '/sys/config/update' : '/sys/config/save'
|
||||
return http.post(http.adornUrl(url), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
export function deleteConfig(ids: number[]): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/config/delete'), http.adornData(ids, false)).then(res => res.data)
|
||||
}
|
||||
|
||||
// ==================== 日志 ====================
|
||||
|
||||
export function getLogList(params?: PageParams): Promise<ApiResponse<PageResult<LogInfo>>> {
|
||||
return http.get(http.adornUrl('/sys/log/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** 审计日志;分页结构含 totalCount、list 等,与系统日志分页一致 */
|
||||
export function getAuditLogList(params?: PageParams): Promise<ApiResponse<PageResult<AuditLogInfo>>> {
|
||||
return http.get(http.adornUrl('/sys/auditLog/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
/** API 调用日志(JST 等);分页结构与审计日志一致 */
|
||||
export function getApiJstCallLogList(params?: PageParams): Promise<ApiResponse<PageResult<ApiJstCallLogInfo>>> {
|
||||
return http.get(http.adornUrl('/sys/apiJstCallLog/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
// ==================== 定时任务 ====================
|
||||
|
||||
export function getScheduleList(params?: PageParams): Promise<ApiResponse<PageResult<ScheduleInfo>>> {
|
||||
return http.get(http.adornUrl('/sys/schedule/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
export function getScheduleInfo(id: number): Promise<ApiResponse<ScheduleInfo>> {
|
||||
return http.get(http.adornUrl(`/sys/schedule/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
export function saveSchedule(data: Partial<ScheduleInfo>): Promise<ApiResponse<any>> {
|
||||
const url = data.jobId ? '/sys/schedule/update' : '/sys/schedule/save'
|
||||
return http.post(http.adornUrl(url), http.adornData(data)).then(res => res.data)
|
||||
}
|
||||
|
||||
export function deleteSchedule(ids: number[]): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/schedule/delete'), http.adornData(ids, false)).then(res => res.data)
|
||||
}
|
||||
|
||||
export function pauseSchedule(jobId: number): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/schedule/pause'), http.adornData({ jobId })).then(res => res.data)
|
||||
}
|
||||
|
||||
export function resumeSchedule(jobId: number): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/schedule/resume'), http.adornData({ jobId })).then(res => res.data)
|
||||
}
|
||||
|
||||
export function runSchedule(jobId: number): Promise<ApiResponse<any>> {
|
||||
return http.post(http.adornUrl('/sys/schedule/run'), http.adornData({ jobId })).then(res => res.data)
|
||||
}
|
||||
|
||||
// ==================== 定时任务日志 ====================
|
||||
|
||||
export function getScheduleLogList(params?: PageParams): Promise<ApiResponse<PageResult<ScheduleLogInfo>>> {
|
||||
return http.get(http.adornUrl('/sys/scheduleLog/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
|
||||
}
|
||||
|
||||
export function getScheduleLogInfo(id: number): Promise<ApiResponse<ScheduleLogInfo>> {
|
||||
return http.get(http.adornUrl(`/sys/scheduleLog/info/${id}`), { params: http.adornParams() }).then(res => res.data)
|
||||
}
|
||||
|
||||
// ==================== OSS 上传 ====================
|
||||
|
||||
/** 直传 COS:GET STS 凭证后前端 putObject,成功返回 data.url(与后端下发 key 一致) */
|
||||
export async function uploadOssDedupe (file: File): Promise<ApiResponse<{ url: string }>> {
|
||||
try {
|
||||
const url = await uploadFileViaStsCos(file)
|
||||
return { code: 0, msg: 'success', data: { url } }
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : '上传失败'
|
||||
return { code: 500, msg: message, data: { url: '' } }
|
||||
}
|
||||
}
|
||||
|
||||
/** 直传 COS 且保留原始文件名(版本 APK 等场景,需后端 sts-credential 支持 keepOriginalFileName) */
|
||||
export async function uploadOssKeepFileName (file: File): Promise<ApiResponse<{ url: string }>> {
|
||||
try {
|
||||
const url = await uploadFileViaStsCos(file, { keepOriginalFileName: true })
|
||||
return { code: 0, msg: 'success', data: { url } }
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : '上传失败'
|
||||
return { code: 500, msg: message, data: { url: '' } }
|
||||
}
|
||||
}
|
||||
BIN
src/assets/img/avatar.png
Executable file
|
After Width: | Height: | Size: 90 KiB |
BIN
src/assets/img/login_bg.jpg
Executable file
|
After Width: | Height: | Size: 142 KiB |
441
src/assets/scss/_base.scss
Executable file
@@ -0,0 +1,441 @@
|
||||
@use "sass:color";
|
||||
@use "./variables" as v;
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.15;
|
||||
color: #303133;
|
||||
background-color: #fff;
|
||||
}
|
||||
a {
|
||||
color: color.mix(#fff, v.$color-primary, 20%);
|
||||
text-decoration: none;
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: v.$color-primary;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
/* Utils
|
||||
------------------------------ */
|
||||
.clearfix:before,
|
||||
.clearfix:after {
|
||||
content: " ";
|
||||
display: table;
|
||||
}
|
||||
.clearfix:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
|
||||
/* Animation
|
||||
------------------------------ */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity .5s;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Reset element-ui
|
||||
------------------------------ */
|
||||
.site-wrapper {
|
||||
.el-pagination {
|
||||
margin-top: 15px;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格+分页:分页在表格下方靠右(表格右下角) */
|
||||
.table-page-wrap {
|
||||
.el-pagination {
|
||||
margin-top: 15px;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全局:所有分页均在表格/内容右下 */
|
||||
.el-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
|
||||
/* Layout
|
||||
------------------------------ */
|
||||
.site-wrapper {
|
||||
position: relative;
|
||||
min-width: 1180px;
|
||||
}
|
||||
|
||||
|
||||
/* Sidebar fold
|
||||
------------------------------ */
|
||||
.site-sidebar--fold {
|
||||
.site-navbar__header,
|
||||
.site-sidebar,
|
||||
.site-sidebar__inner,
|
||||
.el-menu.site-sidebar__menu {
|
||||
width: 64px;
|
||||
}
|
||||
.site-navbar__body,
|
||||
.site-content__wrapper {
|
||||
margin-left: 64px;
|
||||
}
|
||||
// 折叠时仅保留文字(显示 brand-mini),隐藏图标按钮
|
||||
.site-navbar__brand {
|
||||
&-lg {
|
||||
display: none;
|
||||
}
|
||||
&-mini {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
.site-navbar__switch {
|
||||
display: none;
|
||||
}
|
||||
.site-sidebar,
|
||||
.site-sidebar__inner {
|
||||
overflow: initial;
|
||||
}
|
||||
.site-sidebar__menu-icon {
|
||||
margin-right: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
// 折叠后只显示图标:隐藏菜单标题中的文字部分
|
||||
.site-sidebar__menu {
|
||||
.el-menu-item span,
|
||||
.el-sub-menu__title span {
|
||||
display: none;
|
||||
}
|
||||
.el-menu-item__title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.site-content--tabs > .el-tabs > .el-tabs__header {
|
||||
left: 64px;
|
||||
}
|
||||
}
|
||||
// animation
|
||||
.site-navbar__header,
|
||||
.site-navbar__brand,
|
||||
.site-navbar__body,
|
||||
.site-sidebar,
|
||||
.site-sidebar__inner,
|
||||
.site-sidebar__menu.el-menu,
|
||||
.site-sidebar__menu-icon,
|
||||
.site-content__wrapper,
|
||||
.site-content--tabs > .el-tabs .el-tabs__header {
|
||||
transition: inline-block .3s, left .3s, width .3s, margin-left .3s, font-size .3s;
|
||||
}
|
||||
|
||||
|
||||
/* Navbar
|
||||
------------------------------ */
|
||||
.site-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 1030;
|
||||
height: 50px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, .08);
|
||||
background-color: v.$navbar-background-color;
|
||||
|
||||
&--inverse {
|
||||
.site-navbar__body {
|
||||
background-color: transparent;
|
||||
}
|
||||
.site-navbar__body > .el-dropdown .el-dropdown-link {
|
||||
color: #fff;
|
||||
}
|
||||
.el-menu {
|
||||
> .el-menu-item,
|
||||
> .el-sub-menu > .el-sub-menu__title {
|
||||
color: #fff;
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: color.mix(#000, v.$navbar-background-color, 15%);
|
||||
}
|
||||
}
|
||||
> .el-menu-item.is-active,
|
||||
> .el-sub-menu.is-active > .el-sub-menu__title {
|
||||
border-bottom-color: color.mix(#fff, v.$navbar-background-color, 85%);
|
||||
}
|
||||
.el-menu-item i,
|
||||
.el-sub-menu__title i,
|
||||
.el-dropdown {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.el-menu--popup-bottom-start {
|
||||
background-color: v.$navbar-background-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 230px;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
}
|
||||
&__brand {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
width: 230px;
|
||||
height: 50px;
|
||||
margin: 0;
|
||||
line-height: 50px;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
color: #fff;
|
||||
|
||||
&-lg,
|
||||
&-mini {
|
||||
margin: 0 5px;
|
||||
color: #fff;
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
&-mini {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&__switch {
|
||||
display: none;
|
||||
font-size: 18px;
|
||||
border-bottom: none !important;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
line-height: 1;
|
||||
}
|
||||
&__avatar {
|
||||
font-size: 18px;
|
||||
border-bottom: none !important;
|
||||
* {
|
||||
vertical-align: inherit;
|
||||
}
|
||||
.el-dropdown__icon,
|
||||
.el-icon {
|
||||
display: none !important;
|
||||
}
|
||||
.el-dropdown-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
> img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-right: 0;
|
||||
object-fit: cover;
|
||||
border-radius: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
&.site-navbar__avatar-wrap .el-dropdown-link > img {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
&__body {
|
||||
position: relative;
|
||||
margin-left: 230px;
|
||||
padding-right: 15px;
|
||||
background-color: #fff;
|
||||
> .el-dropdown.site-navbar__avatar-wrap {
|
||||
float: right;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.el-dropdown__icon,
|
||||
.el-icon {
|
||||
display: none !important;
|
||||
}
|
||||
.el-dropdown-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
> img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
object-fit: cover;
|
||||
border-radius: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__menu {
|
||||
float: left;
|
||||
background-color: transparent;
|
||||
border-bottom: 0;
|
||||
|
||||
&--right {
|
||||
float: right;
|
||||
}
|
||||
a:focus,
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.el-menu-item,
|
||||
.el-sub-menu > .el-sub-menu__title {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
}
|
||||
.el-sub-menu > .el-menu {
|
||||
top: 55px;
|
||||
}
|
||||
.el-badge {
|
||||
display: inline;
|
||||
z-index: 2;
|
||||
&__content {
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Sidebar
|
||||
------------------------------ */
|
||||
.site-sidebar {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 1020;
|
||||
width: 230px;
|
||||
overflow: hidden;
|
||||
|
||||
&--dark,
|
||||
&--dark-popper {
|
||||
background-color: v.$sidebar-background-color-dark;
|
||||
.site-sidebar__menu.el-menu,
|
||||
> .el-menu--popup {
|
||||
background-color: v.$sidebar-background-color-dark;
|
||||
.el-menu-item,
|
||||
.el-sub-menu > .el-sub-menu__title {
|
||||
color: v.$sidebar-color-text-dark;
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: color.mix(#fff, v.$sidebar-color-text-dark, 50%);
|
||||
background-color: color.mix(#fff, v.$sidebar-background-color-dark, 2.5%);
|
||||
}
|
||||
}
|
||||
.el-menu,
|
||||
.el-sub-menu.is-opened {
|
||||
background-color: color.mix(#000, v.$sidebar-background-color-dark, 15%);
|
||||
}
|
||||
.el-menu-item.is-active,
|
||||
.el-sub-menu.is-active > .el-sub-menu__title {
|
||||
color: color.mix(#fff, v.$sidebar-color-text-dark, 80%);
|
||||
}
|
||||
}
|
||||
}
|
||||
&__inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 250px;
|
||||
height: 100%;
|
||||
padding-bottom: 15px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
&__menu.el-menu {
|
||||
width: 230px;
|
||||
border-right: 0;
|
||||
}
|
||||
&__menu-icon {
|
||||
width: 24px;
|
||||
margin-right: 5px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Content
|
||||
------------------------------ */
|
||||
.site-content {
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
|
||||
&__wrapper {
|
||||
position: relative;
|
||||
padding-top: 50px;
|
||||
margin-left: 230px;
|
||||
min-height: 100%;
|
||||
background: v.$content-background-color;
|
||||
}
|
||||
&--tabs {
|
||||
padding: 55px 0 0;
|
||||
}
|
||||
> .el-tabs {
|
||||
> .el-tabs__header {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
left: 230px;
|
||||
right: 0;
|
||||
z-index: 930;
|
||||
padding: 0 55px 0 15px;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .12), 0 0 6px 0 rgba(0, 0, 0, .04);
|
||||
background-color: #fff;
|
||||
> .el-tabs__nav-wrap {
|
||||
margin-bottom: 0;
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .el-tabs__content {
|
||||
padding: 0 15px 15px;
|
||||
> .site-tabs__tools {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
right: 0;
|
||||
z-index: 931;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
font-size: 16px;
|
||||
line-height: 40px;
|
||||
background-color: v.$content-background-color;
|
||||
cursor: pointer;
|
||||
.el-icon--right {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-table__expand-icon {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
}
|
||||
447
src/assets/scss/_normalize.scss
Executable file
@@ -0,0 +1,447 @@
|
||||
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in
|
||||
* IE on Windows Phone and in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers (opinionated).
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
footer,
|
||||
header,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
* 1. Add the correct display in IE.
|
||||
*/
|
||||
|
||||
figcaption,
|
||||
figure,
|
||||
main { /* 1 */
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct margin in IE 8.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Remove the gray background on active links in IE 10.
|
||||
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent; /* 1 */
|
||||
-webkit-text-decoration-skip: objects; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57- and Firefox 39-.
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font style in Android 4.3-.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct background and color in IE 9-.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background-color: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
*/
|
||||
|
||||
audio,
|
||||
video {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in iOS 4-7.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10-.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the overflow in IE.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers (opinionated).
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: sans-serif; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
|
||||
* controls in Android 4.
|
||||
* 2. Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
html [type="button"], /* 1 */
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct display in IE 9-.
|
||||
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10-.
|
||||
* 2. Remove the padding in IE 10-.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-cancel-button,
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in IE 9-.
|
||||
* 1. Add the correct display in Edge, IE, and Firefox.
|
||||
*/
|
||||
|
||||
details, /* 1 */
|
||||
menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Scripting
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
*/
|
||||
|
||||
canvas {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hidden
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10-.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
13
src/assets/scss/_variables.scss
Executable file
@@ -0,0 +1,13 @@
|
||||
// 站点主色(与 Element UI 默认主题一致:#409EFF)
|
||||
// tips: 要达到整站主题修改效果, 请确保[$color-primary]站点主色与[/src/element-ui-theme/index.js]文件中[import './element-[#17B3A3]/index.css']当前主题色一致
|
||||
$color-primary: #409EFF;
|
||||
|
||||
// Navbar
|
||||
$navbar-background-color: $color-primary;
|
||||
|
||||
// Sidebar
|
||||
$sidebar-background-color-dark: #263238;
|
||||
$sidebar-color-text-dark: #8a979e;
|
||||
|
||||
// Content
|
||||
$content-background-color: #f1f4f5;
|
||||
4
src/assets/scss/index.scss
Executable file
@@ -0,0 +1,4 @@
|
||||
// Dart Sass 推荐使用 @use 替代已弃用的 @import
|
||||
@use "./normalize";
|
||||
@use "./variables";
|
||||
@use "./base";
|
||||
88
src/components/NearExpiryUnusedCardsDialog.vue
Executable file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="nearExpiryDialogVisible"
|
||||
title="临期未使用卡"
|
||||
width="860px"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
:close-on-click-modal="false">
|
||||
<el-alert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="near-expiry-alert">
|
||||
以下卡尚未使用,且剩余激活时间 ≤ 31 天,请尽快安排使用或转移,避免过期浪费。
|
||||
</el-alert>
|
||||
<el-table
|
||||
:data="nearExpiryUnusedList"
|
||||
border
|
||||
v-loading="nearExpiryLoading"
|
||||
row-key="id"
|
||||
max-height="420"
|
||||
style="width: 100%">
|
||||
<el-table-column header-align="center" align="center" width="70" label="序号">
|
||||
<template #default="scope">{{ scope.$index + 1 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cardNo" header-align="center" align="center" min-width="180" label="卡号" show-overflow-tooltip />
|
||||
<el-table-column prop="cardType" header-align="center" align="center" width="80" label="卡类型">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.cardType === 'YEAR'" size="small">年</el-tag>
|
||||
<el-tag v-else-if="row.cardType === 'QUARTER'" size="small" type="success">季</el-tag>
|
||||
<el-tag v-else-if="row.cardType === 'MONTH'" size="small" type="warning">月</el-tag>
|
||||
<el-tag v-else-if="row.cardType === 'WEEK'" size="small" type="info">周</el-tag>
|
||||
<el-tag v-else size="small" type="info">天</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orgName" header-align="center" align="center" min-width="120" label="所属组织" show-overflow-tooltip />
|
||||
<el-table-column prop="activationExpireAt" header-align="center" align="center" min-width="165" label="激活截止时间" show-overflow-tooltip />
|
||||
<el-table-column header-align="center" align="center" width="100" label="剩余天数">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
size="small"
|
||||
:type="row.remainingActivationDays <= 7 ? 'danger' : row.remainingActivationDays <= 14 ? 'warning' : 'info'">
|
||||
{{ row.remainingActivationDays ?? '-' }} 天
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" header-align="center" align="center" width="80" label="操作">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="showDetail(row.id)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!nearExpiryLoading && !nearExpiryUnusedList.length" description="暂无临期未使用卡" />
|
||||
<template #footer>
|
||||
<el-button @click="nearExpiryDialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<card-detail v-model="detailVisible" :card-id="detailId" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import CardDetail from '@/views/modules/card/components/card-detail.vue'
|
||||
import { useNearExpiryUnusedCards } from '@/composables/useNearExpiryUnusedCards'
|
||||
|
||||
defineOptions({ name: 'NearExpiryUnusedCardsDialog' })
|
||||
|
||||
const {
|
||||
nearExpiryUnusedList,
|
||||
nearExpiryLoading,
|
||||
nearExpiryDialogVisible
|
||||
} = useNearExpiryUnusedCards()
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detailId = ref<number | null>(null)
|
||||
|
||||
function showDetail (id: number): void {
|
||||
detailId.value = id
|
||||
detailVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.near-expiry-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
49
src/components/agent-store-select/index.data.ts
Executable file
@@ -0,0 +1,49 @@
|
||||
import type { StoreInfo } from '@/api/store'
|
||||
|
||||
/** AgentStoreSelect 组件 Props */
|
||||
export interface AgentStoreSelectProps {
|
||||
/** 渠道组织 ID(CHANNEL) */
|
||||
channelId?: string | number | null
|
||||
/** 代理商组织 ID(SERVICE_PROVIDER) */
|
||||
agentOrgId?: string | number | null
|
||||
/** 当前选中的值 */
|
||||
modelValue?: string | number | null
|
||||
/** 占位符文本 */
|
||||
placeholder?: string
|
||||
/** 需要先选择代理商时的占位符 */
|
||||
placeholderNeedAgent?: string
|
||||
/** 需要先选择渠道或代理商时的占位符 */
|
||||
placeholderNeedScope?: string
|
||||
/** 是否可搜索 */
|
||||
filterable?: boolean
|
||||
/** 是否可清空 */
|
||||
clearable?: boolean
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean
|
||||
/** 选择器宽度 */
|
||||
width?: string
|
||||
/** 是否自动加载 */
|
||||
autoLoad?: boolean
|
||||
/** 无数据时的占位文本 */
|
||||
emptyText?: string
|
||||
}
|
||||
|
||||
/** 门店选项 */
|
||||
export interface StoreOption {
|
||||
/** 门店 ID */
|
||||
id: string | number
|
||||
/** 门店名称 */
|
||||
name: string
|
||||
/** 原始数据 */
|
||||
raw: StoreInfo
|
||||
}
|
||||
|
||||
/** AgentStoreSelect 组件事件 */
|
||||
export interface AgentStoreSelectEmits {
|
||||
/** 更新 modelValue */
|
||||
(e: 'update:modelValue', value: string | number | null): void
|
||||
/** 值改变时触发 */
|
||||
(e: 'change', value: string | number | null, raw: StoreInfo | null): void
|
||||
/** 选项加载完成时触发 */
|
||||
(e: 'loaded', options: StoreOption[]): void
|
||||
}
|
||||
226
src/components/agent-store-select/index.vue
Executable file
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<el-select
|
||||
:model-value="modelValue"
|
||||
:placeholder="inputPlaceholder"
|
||||
:filterable="filterable"
|
||||
:clearable="clearable"
|
||||
:disabled="selectDisabled"
|
||||
:loading="storeListLoading"
|
||||
:style="selectStyle"
|
||||
@update:model-value="onValueChange"
|
||||
@focus="onFocus"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in storeOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getStoreList, getStoreListByAgent, searchStore } from '@/api/store'
|
||||
import type { StoreInfo } from '@/api/store'
|
||||
import type { AgentStoreSelectProps, StoreOption } from './index.data'
|
||||
|
||||
const props = withDefaults(defineProps<AgentStoreSelectProps>(), {
|
||||
channelId: null,
|
||||
agentOrgId: null,
|
||||
modelValue: '',
|
||||
placeholder: '请选择门店',
|
||||
placeholderNeedAgent: '请先选择代理商',
|
||||
placeholderNeedScope: '请先选择渠道或代理商',
|
||||
filterable: true,
|
||||
clearable: true,
|
||||
disabled: false,
|
||||
width: '100%',
|
||||
autoLoad: true,
|
||||
emptyText: '暂无门店数据'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | number | null]
|
||||
change: [value: string | number | null, raw: StoreInfo | null]
|
||||
loaded: [options: StoreOption[]]
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const storeOptions = ref<StoreOption[]>([])
|
||||
const storeListLoading = ref(false)
|
||||
|
||||
const selectStyle = computed(() => ({
|
||||
width: props.width
|
||||
}))
|
||||
|
||||
/** 已选代理商,或已选渠道 */
|
||||
const hasScope = computed(() => {
|
||||
if (props.agentOrgId != null && props.agentOrgId !== '') return true
|
||||
if (props.channelId != null && props.channelId !== '') return true
|
||||
return false
|
||||
})
|
||||
|
||||
/** 平台/超管:须先选渠道或代理商再选门店 */
|
||||
const selectDisabled = computed(() => {
|
||||
if (props.disabled) return true
|
||||
if (userStore.isPlatform) {
|
||||
return !hasScope.value
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const inputPlaceholder = computed(() => {
|
||||
if (userStore.isPlatform && !hasScope.value) {
|
||||
return props.placeholderNeedScope
|
||||
}
|
||||
if (!hasScope.value) return props.placeholder
|
||||
if (storeListLoading.value) return '加载中…'
|
||||
if (storeOptions.value.length === 0) return props.emptyText
|
||||
return props.placeholder
|
||||
})
|
||||
|
||||
function parseStoreListPayload(data: any): any[] {
|
||||
if (!data) return []
|
||||
const codeOk = data.code === 0 || data.code === undefined
|
||||
if (!codeOk) return []
|
||||
if (Array.isArray(data)) return data
|
||||
if (Array.isArray(data.list)) return data.list
|
||||
if (Array.isArray(data.data?.list)) return data.data.list
|
||||
if (Array.isArray(data.page?.list)) return data.page.list
|
||||
if (Array.isArray(data.data)) return data.data
|
||||
return []
|
||||
}
|
||||
|
||||
/** /manage/store/search 分页结果 */
|
||||
function parseSearchStoreList(data: any): any[] {
|
||||
if (!data) return []
|
||||
const codeOk = data.code === 0 || data.code === undefined
|
||||
if (!codeOk) return []
|
||||
const page = data.page || data.data?.page
|
||||
if (page && Array.isArray(page.list)) return page.list
|
||||
return []
|
||||
}
|
||||
|
||||
function mapToStoreOptions(list: any[]): StoreOption[] {
|
||||
return (list || []).map((item) => ({
|
||||
id: item.id ?? item.storeId ?? item.orgId ?? item.storeOrgId,
|
||||
name: item.name ?? item.storeName ?? item.orgName ?? '-',
|
||||
raw: item
|
||||
})).filter((item) => item.id != null)
|
||||
}
|
||||
|
||||
function loadStoreOptions(): Promise<void> {
|
||||
const aid = props.agentOrgId
|
||||
if (aid != null && aid !== '') {
|
||||
storeListLoading.value = true
|
||||
return getStoreListByAgent(Number(aid))
|
||||
.then(({ data }) => {
|
||||
const list = parseStoreListPayload(data)
|
||||
storeOptions.value = mapToStoreOptions(list)
|
||||
emit('loaded', storeOptions.value)
|
||||
})
|
||||
.catch(() => {
|
||||
storeOptions.value = []
|
||||
emit('loaded', [])
|
||||
})
|
||||
.finally(() => {
|
||||
storeListLoading.value = false
|
||||
})
|
||||
}
|
||||
const cid = props.channelId
|
||||
if (cid != null && cid !== '') {
|
||||
storeListLoading.value = true
|
||||
return searchStore({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
orgId: typeof cid === 'number' ? cid : Number(cid)
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const list = parseSearchStoreList(data)
|
||||
storeOptions.value = mapToStoreOptions(list)
|
||||
emit('loaded', storeOptions.value)
|
||||
})
|
||||
.catch(() => {
|
||||
storeOptions.value = []
|
||||
emit('loaded', [])
|
||||
})
|
||||
.finally(() => {
|
||||
storeListLoading.value = false
|
||||
})
|
||||
}
|
||||
/** 平台/超管:无渠道且未选代理商时不拉全量 */
|
||||
if (userStore.isPlatform) {
|
||||
storeOptions.value = []
|
||||
return Promise.resolve()
|
||||
}
|
||||
/** 非平台:无渠道/代理商时拉全量门店 */
|
||||
storeListLoading.value = true
|
||||
return getStoreList()
|
||||
.then(({ data }) => {
|
||||
const list = parseStoreListPayload(data)
|
||||
storeOptions.value = mapToStoreOptions(list)
|
||||
emit('loaded', storeOptions.value)
|
||||
})
|
||||
.catch(() => {
|
||||
storeOptions.value = []
|
||||
emit('loaded', [])
|
||||
})
|
||||
.finally(() => {
|
||||
storeListLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
if (!props.autoLoad || storeOptions.value.length > 0) return
|
||||
if (userStore.isPlatform && !hasScope.value) return
|
||||
loadStoreOptions()
|
||||
}
|
||||
|
||||
function onValueChange(val: string | number) {
|
||||
emit('update:modelValue', val)
|
||||
const selected = storeOptions.value.find((item) => item.id === val)
|
||||
emit('change', val, selected?.raw ?? null)
|
||||
}
|
||||
|
||||
function refresh(): Promise<void> {
|
||||
storeOptions.value = []
|
||||
return loadStoreOptions()
|
||||
}
|
||||
|
||||
/** 外部补一条选项 */
|
||||
function mergeOption(opt: { id: string | number; name: string; raw?: any }) {
|
||||
if (!opt || opt.id == null || opt.id === '') return
|
||||
const id = opt.id
|
||||
const name = opt.name ?? '-'
|
||||
if (storeOptions.value.some((o) => o.id === id)) return
|
||||
storeOptions.value = [...storeOptions.value, { id, name, raw: opt.raw ?? opt }]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.channelId, props.agentOrgId],
|
||||
() => {
|
||||
emit('update:modelValue', null)
|
||||
storeOptions.value = []
|
||||
if (props.autoLoad) {
|
||||
if (userStore.isPlatform && !hasScope.value) return
|
||||
loadStoreOptions()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
loadStoreOptions,
|
||||
mergeOption
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.autoLoad) return
|
||||
if (hasScope.value) {
|
||||
loadStoreOptions()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
48
src/components/card-flow-from-to-org-select/index.data.ts
Executable file
@@ -0,0 +1,48 @@
|
||||
/** 卡流流出/流入组织选择器 Props */
|
||||
export interface CardFlowFromToOrgSelectProps {
|
||||
/** 流出组织 ID */
|
||||
fromOrgId?: string | number | null
|
||||
/** 流入组织 ID */
|
||||
toOrgId?: string | number | null
|
||||
/** 流出组织表单项标签 */
|
||||
fromLabel?: string
|
||||
/** 流入组织表单项标签 */
|
||||
toLabel?: string
|
||||
/** 选择器宽度 */
|
||||
width?: string
|
||||
/** 是否 inline 表单布局(渲染 el-form-item) */
|
||||
inline?: boolean
|
||||
/** 流出组织占位符 */
|
||||
fromPlaceholder?: string
|
||||
/** 流入组织占位符 */
|
||||
toPlaceholder?: string
|
||||
/** 加载完成后是否自动选中默认流出组织 */
|
||||
autoDefaultFrom?: boolean
|
||||
}
|
||||
|
||||
/** 组织行(扁平列表项) */
|
||||
export interface CardFlowOrgRow {
|
||||
id: string | number
|
||||
orgId?: string | number
|
||||
name?: string
|
||||
type?: string
|
||||
orgType?: string
|
||||
parentId?: string | number
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
children?: CardFlowOrgRow[]
|
||||
}
|
||||
|
||||
export interface CardFlowFromToOrgChangePayload {
|
||||
fromOrgId: string | number | null
|
||||
toOrgId: string | number | null
|
||||
fromOrgName: string | null
|
||||
toOrgName: string | null
|
||||
}
|
||||
|
||||
export const CARD_FLOW_ORG_TYPE_LABEL: Record<string, string> = {
|
||||
PLATFORM: '平台',
|
||||
CHANNEL: '渠道',
|
||||
SERVICE_PROVIDER: '代理商',
|
||||
STORE: '门店'
|
||||
}
|
||||
365
src/components/card-flow-from-to-org-select/index.vue
Executable file
@@ -0,0 +1,365 @@
|
||||
<template>
|
||||
<template v-if="inline">
|
||||
<el-form-item :label="fromLabel" prop="fromOrgId">
|
||||
<el-tree-select
|
||||
:model-value="fromOrgId"
|
||||
:data="sellerTreeData"
|
||||
:props="{ label: 'label', value: 'id', disabled: 'disabled' }"
|
||||
node-key="id"
|
||||
:placeholder="fromPlaceholder"
|
||||
filterable
|
||||
clearable
|
||||
default-expand-all
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
:style="{ width }"
|
||||
@update:model-value="onFromUpdate"
|
||||
@focus="fetchOrgTreeData" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="toLabel" prop="toOrgId">
|
||||
<org-tree-select
|
||||
ref="toOrgTreeSelectRef"
|
||||
:model-value="toOrgId"
|
||||
:placeholder="toPlaceholder"
|
||||
:width="width"
|
||||
@update:model-value="onToUpdate"
|
||||
@change="onToOrgTreeChange" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<div v-else class="card-flow-from-to-org-select">
|
||||
<el-tree-select
|
||||
:model-value="fromOrgId"
|
||||
:data="sellerTreeData"
|
||||
:props="{ label: 'label', value: 'id', disabled: 'disabled' }"
|
||||
node-key="id"
|
||||
:placeholder="fromPlaceholder"
|
||||
filterable
|
||||
clearable
|
||||
default-expand-all
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
:style="{ width }"
|
||||
@update:model-value="onFromUpdate"
|
||||
@focus="fetchOrgTreeData" />
|
||||
<org-tree-select
|
||||
ref="toOrgTreeSelectRef"
|
||||
:model-value="toOrgId"
|
||||
:placeholder="toPlaceholder"
|
||||
:width="width"
|
||||
@update:model-value="onToUpdate"
|
||||
@change="onToOrgTreeChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getOrgListData, parseOrgListDataBody } from '@/api/org'
|
||||
import { treeDataTranslate } from '@/utils'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
import OrgTreeSelect from '@/components/org-tree-select/index.vue'
|
||||
import type { OrgInfo } from '@/api/org'
|
||||
import {
|
||||
CARD_FLOW_ORG_TYPE_LABEL,
|
||||
type CardFlowFromToOrgChangePayload,
|
||||
type CardFlowFromToOrgSelectProps,
|
||||
type CardFlowOrgRow
|
||||
} from './index.data'
|
||||
|
||||
const props = withDefaults(defineProps<CardFlowFromToOrgSelectProps>(), {
|
||||
fromOrgId: null,
|
||||
toOrgId: null,
|
||||
fromLabel: '流出组织',
|
||||
toLabel: '流入组织',
|
||||
width: '220px',
|
||||
inline: false,
|
||||
fromPlaceholder: '请选择流出组织(平台/渠道/代理商)',
|
||||
toPlaceholder: '请选择流入组织',
|
||||
autoDefaultFrom: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:fromOrgId': [value: string | number | null]
|
||||
'update:toOrgId': [value: string | number | null]
|
||||
change: [payload: CardFlowFromToOrgChangePayload]
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const orgFlatList = ref<CardFlowOrgRow[]>([])
|
||||
const sellerTreeData = ref<CardFlowOrgRow[]>([])
|
||||
const allowedSellerIdSet = ref<Set<string>>(new Set())
|
||||
const toOrgTreeSelectRef = ref<InstanceType<typeof OrgTreeSelect> | null>(null)
|
||||
const toOrgNameCache = ref<string | null>(null)
|
||||
|
||||
function rowType (row: CardFlowOrgRow): string {
|
||||
return String(row?.type ?? row?.orgType ?? '').toUpperCase()
|
||||
}
|
||||
|
||||
function isSellerOrg (row: CardFlowOrgRow): boolean {
|
||||
const t = rowType(row)
|
||||
return t === 'PLATFORM' || t === 'CHANNEL' || t === 'SERVICE_PROVIDER'
|
||||
}
|
||||
|
||||
function isStoreOrg (row: CardFlowOrgRow): boolean {
|
||||
return rowType(row) === 'STORE'
|
||||
}
|
||||
|
||||
function optionLabel (row: CardFlowOrgRow): string {
|
||||
const t = CARD_FLOW_ORG_TYPE_LABEL[rowType(row)] || rowType(row) || '-'
|
||||
return `${row.name || '-'}(${t})`
|
||||
}
|
||||
|
||||
function parentKeyOf (o: CardFlowOrgRow): string | null {
|
||||
const p = o.parentId
|
||||
if (p == null || p === '' || String(p) === '0') return null
|
||||
return String(p)
|
||||
}
|
||||
|
||||
function resolvePlatformOrgId (list: CardFlowOrgRow[]): string | number | null {
|
||||
const hit = list.find((o) => rowType(o) === 'PLATFORM')
|
||||
return hit?.id ?? hit?.orgId ?? null
|
||||
}
|
||||
|
||||
function resolveDataScopeRootId (list: CardFlowOrgRow[], userOrgId: string | number | null): string | number | null {
|
||||
if (userOrgId == null || userOrgId === '' || String(userOrgId) === '0') return null
|
||||
const byId = new Map<string, CardFlowOrgRow>(list.map((o) => [String(o.id), o]))
|
||||
let cur: string | number | null | undefined = userOrgId
|
||||
let top: string | number | null = null
|
||||
const seen = new Set<string>()
|
||||
while (cur != null && cur !== '' && String(cur) !== '0') {
|
||||
const key = String(cur)
|
||||
if (seen.has(key)) break
|
||||
seen.add(key)
|
||||
top = cur
|
||||
const row = byId.get(key)
|
||||
if (!row) break
|
||||
const p = row.parentId
|
||||
if (p == null || p === '' || String(p) === '0') break
|
||||
if (!byId.has(String(p))) break
|
||||
cur = p
|
||||
}
|
||||
return top
|
||||
}
|
||||
|
||||
function collectSubtreeIncludingSelf (list: CardFlowOrgRow[], rootId: string | number): CardFlowOrgRow[] {
|
||||
if (rootId == null || rootId === '') return []
|
||||
const byParent = new Map<string | null, CardFlowOrgRow[]>()
|
||||
for (const o of list) {
|
||||
const pk = parentKeyOf(o)
|
||||
if (!byParent.has(pk)) byParent.set(pk, [])
|
||||
byParent.get(pk)!.push(o)
|
||||
}
|
||||
const out: CardFlowOrgRow[] = []
|
||||
const seen = new Set<string>()
|
||||
const q = [String(rootId)]
|
||||
while (q.length) {
|
||||
const id = q.shift()
|
||||
if (!id || seen.has(id)) continue
|
||||
seen.add(id)
|
||||
const node = list.find((n) => String(n.id) === id)
|
||||
if (!node) continue
|
||||
out.push(node)
|
||||
const kids = byParent.get(id) || []
|
||||
for (const k of kids) q.push(String(k.id))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function mergeSubtreesFromTopRoots (list: CardFlowOrgRow[]): CardFlowOrgRow[] {
|
||||
const roots = list.filter((o) => parentKeyOf(o) == null)
|
||||
const m = new Map<string, CardFlowOrgRow>()
|
||||
for (const r of roots) {
|
||||
for (const n of collectSubtreeIncludingSelf(list, r.id)) {
|
||||
m.set(String(n.id), n)
|
||||
}
|
||||
}
|
||||
return [...m.values()]
|
||||
}
|
||||
|
||||
function flatListHidingStores (flatList: CardFlowOrgRow[]): CardFlowOrgRow[] {
|
||||
if (!flatList?.length) return []
|
||||
const storeIds = new Set(
|
||||
flatList.filter((o) => isStoreOrg(o)).map((o) => String(o.id))
|
||||
)
|
||||
if (!storeIds.size) {
|
||||
return flatList.map((n) => ({ ...n, parentId: n.parentId }))
|
||||
}
|
||||
const byId = new Map(flatList.map((o) => [String(o.id), o]))
|
||||
function climbPastStores (pid: string | number | null | undefined): string | number | undefined {
|
||||
let p = pid
|
||||
while (
|
||||
p != null &&
|
||||
p !== '' &&
|
||||
String(p) !== '0' &&
|
||||
storeIds.has(String(p))
|
||||
) {
|
||||
const parent = byId.get(String(p))
|
||||
if (!parent) break
|
||||
p = parent.parentId
|
||||
}
|
||||
if (p == null || p === '' || String(p) === '0') return undefined
|
||||
return p
|
||||
}
|
||||
return flatList
|
||||
.filter((o) => !isStoreOrg(o))
|
||||
.map((o) => {
|
||||
const newPid = climbPastStores(o.parentId)
|
||||
return { ...o, parentId: newPid }
|
||||
})
|
||||
}
|
||||
|
||||
function decorateSellerTreeNodes (nodes: CardFlowOrgRow[]): CardFlowOrgRow[] {
|
||||
if (!nodes?.length) return []
|
||||
return nodes.map((node) => {
|
||||
const label = optionLabel(node)
|
||||
const disabled = !isSellerOrg(node)
|
||||
const item: CardFlowOrgRow = { ...node, label, disabled }
|
||||
if (node.children?.length) {
|
||||
item.children = decorateSellerTreeNodes(node.children)
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
function buildSellerTreeData (flatSubtree: CardFlowOrgRow[]): CardFlowOrgRow[] {
|
||||
if (!flatSubtree?.length) return []
|
||||
const withoutStores = flatListHidingStores(flatSubtree)
|
||||
if (!withoutStores.length) return []
|
||||
const tree = treeDataTranslate(withoutStores, 'id', 'parentId') as CardFlowOrgRow[]
|
||||
return decorateSellerTreeNodes(tree)
|
||||
}
|
||||
|
||||
function resolveAllocatorRootId (list: CardFlowOrgRow[]): string | number | null {
|
||||
const p = resolvePlatformOrgId(list)
|
||||
if (p != null) return p
|
||||
const roots = list.filter((o) => parentKeyOf(o) == null)
|
||||
if (roots.length === 1) return roots[0].id
|
||||
return null
|
||||
}
|
||||
|
||||
function findOrgName (orgId: string | number | null): string | null {
|
||||
if (orgId == null || orgId === '') return null
|
||||
const hit = orgFlatList.value.find((o) => String(o.id) === String(orgId))
|
||||
return hit?.name ?? null
|
||||
}
|
||||
|
||||
function emitChange (fromOrgId: string | number | null, toOrgId: string | number | null): void {
|
||||
emit('change', {
|
||||
fromOrgId,
|
||||
toOrgId,
|
||||
fromOrgName: findOrgName(fromOrgId),
|
||||
toOrgName: toOrgId != null && toOrgId !== ''
|
||||
? (toOrgNameCache.value ?? findOrgName(toOrgId))
|
||||
: null
|
||||
})
|
||||
}
|
||||
|
||||
function onFromUpdate (val: string | number | null): void {
|
||||
emit('update:fromOrgId', val)
|
||||
emitChange(val, props.toOrgId ?? null)
|
||||
}
|
||||
|
||||
function onToUpdate (val: string | number | null): void {
|
||||
emit('update:toOrgId', val)
|
||||
if (val == null || val === '') {
|
||||
toOrgNameCache.value = null
|
||||
}
|
||||
emitChange(props.fromOrgId ?? null, val)
|
||||
}
|
||||
|
||||
function onToOrgTreeChange (payload: { node: OrgInfo | null }): void {
|
||||
toOrgNameCache.value = payload.node?.name ?? null
|
||||
emitChange(props.fromOrgId ?? null, props.toOrgId ?? null)
|
||||
}
|
||||
|
||||
function fetchOrgTreeData (): void {
|
||||
if (orgFlatList.value.length > 0) return
|
||||
getOrgListData()
|
||||
.then((res) => {
|
||||
const data = res.data as ApiResponse
|
||||
if (!data || Number(data.code) !== 0) {
|
||||
orgFlatList.value = []
|
||||
sellerTreeData.value = []
|
||||
allowedSellerIdSet.value = new Set()
|
||||
return
|
||||
}
|
||||
const list = parseOrgListDataBody(data as unknown as Record<string, unknown>) as CardFlowOrgRow[]
|
||||
orgFlatList.value = list
|
||||
|
||||
let ancestorNodes: CardFlowOrgRow[]
|
||||
let defaultParentId: string | number | null
|
||||
if (!userStore.isPlatform) {
|
||||
const oid = userStore.orgId
|
||||
if (oid == null || String(oid) === '0') {
|
||||
sellerTreeData.value = []
|
||||
allowedSellerIdSet.value = new Set()
|
||||
return
|
||||
}
|
||||
const scopeRoot = resolveDataScopeRootId(list, oid)
|
||||
if (scopeRoot == null || scopeRoot === '') {
|
||||
sellerTreeData.value = []
|
||||
allowedSellerIdSet.value = new Set()
|
||||
return
|
||||
}
|
||||
ancestorNodes = collectSubtreeIncludingSelf(list, scopeRoot)
|
||||
defaultParentId = oid
|
||||
} else {
|
||||
const root = resolveAllocatorRootId(list)
|
||||
if (root != null) {
|
||||
ancestorNodes = collectSubtreeIncludingSelf(list, root)
|
||||
defaultParentId = root
|
||||
} else {
|
||||
ancestorNodes = mergeSubtreesFromTopRoots(list)
|
||||
const roots = list.filter((o) => parentKeyOf(o) == null)
|
||||
defaultParentId = roots.length ? roots[0].id : null
|
||||
}
|
||||
}
|
||||
|
||||
const sellerNodes = (ancestorNodes || []).filter(isSellerOrg)
|
||||
sellerTreeData.value = buildSellerTreeData(ancestorNodes || [])
|
||||
allowedSellerIdSet.value = new Set(sellerNodes.map((o) => String(o.id)))
|
||||
|
||||
if (!sellerNodes.length || !props.autoDefaultFrom) return
|
||||
|
||||
const sellerIdSet = allowedSellerIdSet.value
|
||||
let defSellerId = defaultParentId
|
||||
if (!sellerIdSet.has(String(defSellerId))) {
|
||||
defSellerId = sellerNodes[0].id
|
||||
}
|
||||
if (props.fromOrgId == null || props.fromOrgId === '') {
|
||||
emit('update:fromOrgId', defSellerId)
|
||||
emitChange(defSellerId, props.toOrgId ?? null)
|
||||
} else if (!sellerIdSet.has(String(props.fromOrgId))) {
|
||||
emit('update:fromOrgId', defSellerId)
|
||||
emitChange(defSellerId, props.toOrgId ?? null)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
orgFlatList.value = []
|
||||
sellerTreeData.value = []
|
||||
allowedSellerIdSet.value = new Set()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrgTreeData()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
refresh: () => {
|
||||
orgFlatList.value = []
|
||||
sellerTreeData.value = []
|
||||
allowedSellerIdSet.value = new Set()
|
||||
fetchOrgTreeData()
|
||||
toOrgTreeSelectRef.value?.refresh?.()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-flow-from-to-org-select {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
53
src/components/channel-agent-tree-select/index.data.ts
Executable file
@@ -0,0 +1,53 @@
|
||||
import type { OrgInfo } from '@/api/org'
|
||||
|
||||
/** ChannelAgentTreeSelect 组件 Props */
|
||||
export interface ChannelAgentTreeSelectProps {
|
||||
/** 渠道 ID */
|
||||
channelId?: string | number | null
|
||||
/** 当前选中的值 */
|
||||
modelValue?: string | number | null
|
||||
/** 外部传入的树形数据 */
|
||||
treeData?: OrgInfo[] | null
|
||||
/** 是否自动加载 */
|
||||
autoLoad?: boolean
|
||||
/** 下拉框高度 */
|
||||
height?: string
|
||||
/** 选择器宽度 */
|
||||
width?: string
|
||||
/** 是否默认展开所有节点 */
|
||||
defaultExpandAll?: boolean
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean
|
||||
/** 占位符文本 */
|
||||
placeholder?: string
|
||||
/** 需要先选择渠道时的占位符 */
|
||||
placeholderNeedChannel?: string
|
||||
/** 是否可清空 */
|
||||
clearable?: boolean
|
||||
/** 是否可搜索 */
|
||||
filterable?: boolean
|
||||
/** 无数据时的占位文本 */
|
||||
emptyText?: string
|
||||
}
|
||||
|
||||
/** ChannelAgentTreeSelect 组件事件 */
|
||||
export interface ChannelAgentTreeSelectEmits {
|
||||
/** 更新 modelValue */
|
||||
(e: 'update:modelValue', value: string | number | null): void
|
||||
/** 值改变时触发 */
|
||||
(e: 'change', payload: {
|
||||
node: OrgInfo | null
|
||||
path: OrgInfo[]
|
||||
agentOrgId: string | number | null
|
||||
}): void
|
||||
/** 数据加载完成时触发 */
|
||||
(e: 'loaded', data: OrgInfo[]): void
|
||||
}
|
||||
|
||||
/** 树选择节点 */
|
||||
export interface TreeSelectNode extends OrgInfo {
|
||||
/** 显示标签 */
|
||||
label: string
|
||||
/** 子节点 */
|
||||
children?: TreeSelectNode[]
|
||||
}
|
||||
218
src/components/channel-agent-tree-select/index.vue
Executable file
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div class="channel-agent-tree-select-wrap" v-loading="loading">
|
||||
<el-tree-select
|
||||
:model-value="modelValue"
|
||||
:data="treeSelectData"
|
||||
:props="{ label: 'label', value: 'id', children: 'children' }"
|
||||
node-key="id"
|
||||
check-strictly
|
||||
:placeholder="inputPlaceholder"
|
||||
:clearable="clearable"
|
||||
:filterable="filterable"
|
||||
:disabled="treeSelectDisabled"
|
||||
:render-after-expand="false"
|
||||
:default-expand-all="defaultExpandAll"
|
||||
:style="{ width: width }"
|
||||
popper-class="channel-agent-tree-select-popper"
|
||||
@update:model-value="onUpdateModelValue"
|
||||
@focus="onFocus"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getChannelAgentTree } from '@/api/org'
|
||||
import { treeDataTranslate } from '@/utils'
|
||||
import type { OrgInfo } from '@/api/org'
|
||||
import type { ChannelAgentTreeSelectProps } from './index.data'
|
||||
|
||||
const props = withDefaults(defineProps<ChannelAgentTreeSelectProps>(), {
|
||||
channelId: null,
|
||||
modelValue: null,
|
||||
treeData: null,
|
||||
autoLoad: true,
|
||||
height: '320px',
|
||||
width: '100%',
|
||||
defaultExpandAll: true,
|
||||
disabled: false,
|
||||
placeholder: '请选择代理商',
|
||||
placeholderNeedChannel: '请先选择渠道',
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
emptyText: '暂无代理商数据'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | number | null]
|
||||
change: [payload: { node: OrgInfo | null; path: OrgInfo[]; agentOrgId: string | number | null }]
|
||||
loaded: [data: OrgInfo[]]
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
/** 平台/超管:须先选渠道再选代理商 */
|
||||
const treeSelectDisabled = computed(() => {
|
||||
if (props.disabled) return true
|
||||
if (userStore.isPlatform) {
|
||||
return props.channelId == null || props.channelId === ''
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const rawTree = ref<OrgInfo[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
function parseTreeResponse(data: any): OrgInfo[] {
|
||||
let list: any = []
|
||||
if (Array.isArray(data)) {
|
||||
list = data
|
||||
} else if (data && typeof data === 'object') {
|
||||
const d = data.data
|
||||
if (Array.isArray(d)) list = d
|
||||
else if (d && typeof d === 'object') list = d.tree || d.list || []
|
||||
else list = data.tree || data.list || []
|
||||
}
|
||||
if (!Array.isArray(list)) list = []
|
||||
if (list.length > 0 && list[0].children !== undefined) {
|
||||
return list as OrgInfo[]
|
||||
}
|
||||
return treeDataTranslate(list, 'id', 'parentId') as OrgInfo[]
|
||||
}
|
||||
|
||||
function mapTreeSelectNodes(nodes: OrgInfo[]): any[] {
|
||||
if (!nodes?.length) return []
|
||||
return nodes.map((n) => {
|
||||
const childList = n.children?.length ? mapTreeSelectNodes(n.children) : undefined
|
||||
return {
|
||||
...n,
|
||||
label: n.name || '-',
|
||||
children: childList
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const treeSelectData = computed(() => mapTreeSelectNodes(rawTree.value))
|
||||
|
||||
const inputPlaceholder = computed(() => {
|
||||
if (userStore.isPlatform && (props.channelId == null || props.channelId === '')) {
|
||||
return props.placeholderNeedChannel
|
||||
}
|
||||
if (!props.channelId && props.channelId !== 0) return props.placeholder
|
||||
if (loading.value) return '加载中…'
|
||||
if (treeSelectData.value.length === 0) return props.emptyText
|
||||
return props.placeholder
|
||||
})
|
||||
|
||||
function findPath(nodes: OrgInfo[], targetId: string | number, path: OrgInfo[] = []): OrgInfo[] | null {
|
||||
if (!nodes?.length) return null
|
||||
const sid = String(targetId)
|
||||
for (const n of nodes) {
|
||||
const next = [...path, n]
|
||||
if (String(n.id) === sid) return next
|
||||
const sub = findPath(n.children || [], targetId, next)
|
||||
if (sub) return sub
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function onUpdateModelValue(val: string | number | null) {
|
||||
emit('update:modelValue', val)
|
||||
if (val == null || val === '') {
|
||||
emit('change', { node: null, path: [], agentOrgId: null })
|
||||
return
|
||||
}
|
||||
const path = findPath(rawTree.value, val)
|
||||
const node = path?.length ? path[path.length - 1] : null
|
||||
emit('change', {
|
||||
node,
|
||||
path: path || [],
|
||||
agentOrgId: node?.id ?? val
|
||||
})
|
||||
}
|
||||
|
||||
function loadTree(): Promise<void> {
|
||||
if (props.treeData != null && Array.isArray(props.treeData)) {
|
||||
rawTree.value = props.treeData
|
||||
emit('loaded', rawTree.value)
|
||||
return Promise.resolve()
|
||||
}
|
||||
const cid = props.channelId
|
||||
if (cid == null || cid === '') {
|
||||
rawTree.value = []
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (!props.autoLoad) {
|
||||
rawTree.value = []
|
||||
return Promise.resolve()
|
||||
}
|
||||
loading.value = true
|
||||
return getChannelAgentTree(Number(cid))
|
||||
.then(({ data }) => {
|
||||
const parsed = parseTreeResponse(data)
|
||||
rawTree.value = Array.isArray(parsed) ? parsed : []
|
||||
emit('loaded', rawTree.value)
|
||||
})
|
||||
.catch(() => {
|
||||
rawTree.value = []
|
||||
emit('loaded', [])
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
if (props.autoLoad && props.treeData == null && props.channelId != null && props.channelId !== '' && rawTree.value.length === 0) {
|
||||
loadTree()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.channelId,
|
||||
(next, prev) => {
|
||||
if (next === prev) return
|
||||
if (prev != null && prev !== '') {
|
||||
emit('update:modelValue', null)
|
||||
}
|
||||
rawTree.value = []
|
||||
if (next != null && next !== '') {
|
||||
loadTree()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.treeData,
|
||||
(v) => {
|
||||
if (v != null && Array.isArray(v)) {
|
||||
rawTree.value = v
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
function refresh(): Promise<void> {
|
||||
rawTree.value = []
|
||||
return loadTree()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
loadTree,
|
||||
getDisplayTree: () => rawTree.value
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.channelId != null && props.channelId !== '') {
|
||||
loadTree()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.channel-agent-tree-select-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
43
src/components/channel-select/index.data.ts
Executable file
@@ -0,0 +1,43 @@
|
||||
import type { OrgInfo } from '@/api/org'
|
||||
|
||||
/** ChannelSelect 组件 Props */
|
||||
export interface ChannelSelectProps {
|
||||
/** 当前选中的值 */
|
||||
modelValue?: string | number | null
|
||||
/** 占位符文本 */
|
||||
placeholder?: string
|
||||
/** 是否可搜索 */
|
||||
filterable?: boolean
|
||||
/** 是否可清空 */
|
||||
clearable?: boolean
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean
|
||||
/** 选择器宽度 */
|
||||
width?: string
|
||||
/** 是否自动加载 */
|
||||
autoLoad?: boolean
|
||||
/** 无初始值时,渠道列表加载完成后自动选中第一项 */
|
||||
defaultFirst?: boolean
|
||||
/** 无数据时的占位文本 */
|
||||
emptyText?: string
|
||||
}
|
||||
|
||||
/** 渠道选项 */
|
||||
export interface ChannelOption {
|
||||
/** 渠道 ID */
|
||||
id: string | number
|
||||
/** 渠道显示标签 */
|
||||
label: string
|
||||
/** 原始数据 */
|
||||
raw: OrgInfo
|
||||
}
|
||||
|
||||
/** ChannelSelect 组件事件 */
|
||||
export interface ChannelSelectEmits {
|
||||
/** 更新 modelValue */
|
||||
(e: 'update:modelValue', value: string | number | null): void
|
||||
/** 值改变时触发 */
|
||||
(e: 'change', value: string | number | null, raw: OrgInfo | null): void
|
||||
/** 选项加载完成时触发 */
|
||||
(e: 'loaded', options: ChannelOption[]): void
|
||||
}
|
||||
126
src/components/channel-select/index.vue
Executable file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<el-select
|
||||
:model-value="modelValue"
|
||||
:placeholder="inputPlaceholder"
|
||||
:filterable="filterable"
|
||||
:clearable="clearable"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
:style="selectStyle"
|
||||
@update:model-value="onValueChange"
|
||||
@focus="onFocus"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getOrgChannels } from '@/api/org'
|
||||
import type { OrgInfo } from '@/api/org'
|
||||
import type { ChannelSelectProps, ChannelOption } from './index.data'
|
||||
|
||||
const props = withDefaults(defineProps<ChannelSelectProps>(), {
|
||||
modelValue: null,
|
||||
placeholder: '请选择渠道',
|
||||
filterable: true,
|
||||
clearable: true,
|
||||
disabled: false,
|
||||
width: '100%',
|
||||
autoLoad: true,
|
||||
defaultFirst: false,
|
||||
emptyText: '暂无渠道数据'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | number | null]
|
||||
change: [value: string | number | null, raw: OrgInfo | null]
|
||||
loaded: [options: ChannelOption[]]
|
||||
}>()
|
||||
|
||||
const options = ref<ChannelOption[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const selectStyle = computed(() => ({
|
||||
width: props.width
|
||||
}))
|
||||
|
||||
const inputPlaceholder = computed(() => {
|
||||
if (loading.value) return '加载中…'
|
||||
if (options.value.length === 0) return props.emptyText
|
||||
return props.placeholder
|
||||
})
|
||||
|
||||
function parseChannelsPayload(data: any): any[] {
|
||||
if (!data) return []
|
||||
const codeOk = data.code === 0 || data.code === undefined
|
||||
if (!codeOk) return []
|
||||
if (Array.isArray(data)) return data
|
||||
if (Array.isArray(data.data)) return data.data
|
||||
if (data.data && typeof data.data === 'object' && Array.isArray(data.data.list)) return data.data.list
|
||||
if (Array.isArray(data.list)) return data.list
|
||||
return []
|
||||
}
|
||||
|
||||
function loadOptions(): Promise<void> {
|
||||
if (options.value.length > 0) return Promise.resolve()
|
||||
loading.value = true
|
||||
return getOrgChannels()
|
||||
.then(({ data }) => {
|
||||
const list = parseChannelsPayload(data)
|
||||
options.value = (list || []).map((item: OrgInfo) => ({
|
||||
id: item.id,
|
||||
label: item.name ?? item.orgName ?? '-',
|
||||
raw: item
|
||||
})).filter((item: ChannelOption) => item.id != null)
|
||||
if (
|
||||
props.defaultFirst &&
|
||||
(props.modelValue == null || props.modelValue === '') &&
|
||||
options.value.length > 0
|
||||
) {
|
||||
onValueChange(options.value[0].id)
|
||||
}
|
||||
emit('loaded', options.value)
|
||||
})
|
||||
.catch(() => {
|
||||
options.value = []
|
||||
emit('loaded', [])
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
if (props.autoLoad && options.value.length === 0) {
|
||||
loadOptions()
|
||||
}
|
||||
}
|
||||
|
||||
function onValueChange(val: string | number | null) {
|
||||
emit('update:modelValue', val)
|
||||
const selected = options.value.find((item) => item.id === val)
|
||||
emit('change', val, selected?.raw ?? null)
|
||||
}
|
||||
|
||||
function refresh(): Promise<void> {
|
||||
options.value = []
|
||||
return loadOptions()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
loadOptions
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoLoad) {
|
||||
loadOptions()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
11
src/components/icon-svg/index.data.ts
Executable file
@@ -0,0 +1,11 @@
|
||||
/** IconSvg 组件 Props */
|
||||
export interface IconSvgProps {
|
||||
/** 图标名称 */
|
||||
name: string
|
||||
/** 自定义类名 */
|
||||
className?: string
|
||||
/** 图标宽度 */
|
||||
width?: string
|
||||
/** 图标高度 */
|
||||
height?: string
|
||||
}
|
||||
33
src/components/icon-svg/index.vue
Executable file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<svg
|
||||
:class="getClassName"
|
||||
:width="width"
|
||||
:height="height"
|
||||
aria-hidden="true">
|
||||
<use :xlink:href="getName"></use>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { IconSvgProps } from './index.data'
|
||||
|
||||
const props = defineProps<IconSvgProps>()
|
||||
|
||||
const getName = computed(() => `#icon-${props.name}`)
|
||||
|
||||
const getClassName = computed(() => [
|
||||
'icon-svg',
|
||||
`icon-svg__${props.name}`,
|
||||
props.className && /\S/.test(props.className) ? `${props.className}` : ''
|
||||
])
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.icon-svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
61
src/components/org-tree-select/index.data.ts
Executable file
@@ -0,0 +1,61 @@
|
||||
import type { OrgInfo } from '@/api/org'
|
||||
|
||||
/** OrgTreeSelect 组件 Props */
|
||||
export interface OrgTreeSelectProps {
|
||||
/** 当前选中的值 */
|
||||
modelValue?: string | number | null
|
||||
/** 外部传入的树形数据 */
|
||||
treeData?: OrgInfo[] | null
|
||||
/** 是否自动加载 */
|
||||
autoLoad?: boolean
|
||||
/** 下拉框高度 */
|
||||
height?: string
|
||||
/** 选择器宽度 */
|
||||
width?: string
|
||||
/** 是否默认展开所有节点 */
|
||||
defaultExpandAll?: boolean
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean
|
||||
/** 占位符文本 */
|
||||
placeholder?: string
|
||||
/** 是否可清空 */
|
||||
clearable?: boolean
|
||||
/** 是否可搜索 */
|
||||
filterable?: boolean
|
||||
/** 无数据时的占位文本 */
|
||||
emptyText?: string
|
||||
/** 隐藏指定类型节点 */
|
||||
hideNodeTypes?: string[]
|
||||
}
|
||||
|
||||
/** OrgTreeSelect 组件事件 */
|
||||
export interface OrgTreeSelectEmits {
|
||||
/** 更新 modelValue */
|
||||
(e: 'update:modelValue', value: string | number | null): void
|
||||
/** 值改变时触发 */
|
||||
(e: 'change', payload: {
|
||||
node: OrgInfo | null
|
||||
path: OrgInfo[]
|
||||
orgId: string | number | null
|
||||
channelOrgId: string | number | null
|
||||
agentOrgId: string | number | null
|
||||
}): void
|
||||
/** 数据加载完成时触发 */
|
||||
(e: 'loaded', data: OrgInfo[]): void
|
||||
}
|
||||
|
||||
/** 组织类型标签映射 */
|
||||
export const typeLabelMap: Record<string, string> = {
|
||||
PLATFORM: '平台',
|
||||
CHANNEL: '渠道',
|
||||
SERVICE_PROVIDER: '代理商',
|
||||
STORE: '门店'
|
||||
}
|
||||
|
||||
/** 树选择节点 */
|
||||
export interface TreeSelectNode extends OrgInfo {
|
||||
/** 显示标签 */
|
||||
label: string
|
||||
/** 子节点 */
|
||||
children?: TreeSelectNode[]
|
||||
}
|
||||
254
src/components/org-tree-select/index.vue
Executable file
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<div class="org-tree-select-wrap" v-loading="loading">
|
||||
<el-tree-select
|
||||
:model-value="modelValue"
|
||||
:data="treeSelectData"
|
||||
:props="{ label: 'label', value: 'id', children: 'children' }"
|
||||
node-key="id"
|
||||
check-strictly
|
||||
:placeholder="inputPlaceholder"
|
||||
:clearable="clearable"
|
||||
:filterable="filterable"
|
||||
:disabled="disabled"
|
||||
:render-after-expand="false"
|
||||
:default-expand-all="defaultExpandAll"
|
||||
:style="{ width: width }"
|
||||
popper-class="org-tree-select-popper"
|
||||
@update:model-value="onUpdateModelValue"
|
||||
@focus="onFocus"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { getOrgAllTree as getOrgTree } from '@/api/org'
|
||||
import { treeDataTranslate } from '@/utils'
|
||||
import type { OrgInfo } from '@/api/org'
|
||||
import { typeLabelMap } from './index.data'
|
||||
import type { OrgTreeSelectProps } from './index.data'
|
||||
|
||||
const props = withDefaults(defineProps<OrgTreeSelectProps>(), {
|
||||
modelValue: null,
|
||||
treeData: null,
|
||||
autoLoad: true,
|
||||
height: '320px',
|
||||
width: '100%',
|
||||
defaultExpandAll: true,
|
||||
disabled: false,
|
||||
placeholder: '请选择组织',
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
emptyText: '暂无数据',
|
||||
hideNodeTypes: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | number | null]
|
||||
change: [payload: { node: OrgInfo | null; path: OrgInfo[]; orgId: string | number | null; channelOrgId: string | number | null; agentOrgId: string | number | null }]
|
||||
loaded: [data: OrgInfo[]]
|
||||
}>()
|
||||
|
||||
const rawTree = ref<OrgInfo[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
function normType(type?: string): string {
|
||||
return (type || '').toUpperCase().trim()
|
||||
}
|
||||
|
||||
function typeLabel(type?: string): string {
|
||||
const m: Record<string, string> = typeLabelMap
|
||||
return m[normType(type)] || type || '-'
|
||||
}
|
||||
|
||||
function filterHideTypes(nodes: OrgInfo[], hideSet: Set<string>): OrgInfo[] {
|
||||
if (!nodes?.length) return []
|
||||
const out: OrgInfo[] = []
|
||||
for (const n of nodes) {
|
||||
const t = normType(n.type)
|
||||
if (hideSet.has(t)) {
|
||||
out.push(...filterHideTypes(n.children || [], hideSet))
|
||||
continue
|
||||
}
|
||||
const children = filterHideTypes(n.children || [], hideSet)
|
||||
out.push({
|
||||
...n,
|
||||
children: children.length ? children : undefined
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const displayTree = computed(() => {
|
||||
const hideSet = new Set((props.hideNodeTypes || []).map(normType))
|
||||
return filterHideTypes(rawTree.value, hideSet)
|
||||
})
|
||||
|
||||
function mapTreeSelectNodes(nodes: OrgInfo[]): any[] {
|
||||
if (!nodes?.length) return []
|
||||
return nodes.map((n) => {
|
||||
const childList = n.children?.length ? mapTreeSelectNodes(n.children) : undefined
|
||||
return {
|
||||
...n,
|
||||
label: `${n.name || '-'}(${typeLabel(n.type)})`,
|
||||
children: childList
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const treeSelectData = computed(() => mapTreeSelectNodes(displayTree.value))
|
||||
|
||||
const inputPlaceholder = computed(() => {
|
||||
if (loading.value) return '加载中…'
|
||||
if (treeSelectData.value.length === 0) return props.emptyText
|
||||
return props.placeholder
|
||||
})
|
||||
|
||||
function findPath(nodes: OrgInfo[], targetId: string | number, path: OrgInfo[] = []): OrgInfo[] | null {
|
||||
if (!nodes?.length) return null
|
||||
const sid = String(targetId)
|
||||
for (const n of nodes) {
|
||||
const next = [...path, n]
|
||||
if (String(n.id) === sid) return next
|
||||
const sub = findPath(n.children || [], targetId, next)
|
||||
if (sub) return sub
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function deriveBindings(path: OrgInfo[]) {
|
||||
if (!path?.length) {
|
||||
return { orgId: null as string | number | null, channelOrgId: null as string | number | null, agentOrgId: null as string | number | null }
|
||||
}
|
||||
let channelOrgId: string | number | null = null
|
||||
let agentOrgId: string | number | null = null
|
||||
for (const n of path) {
|
||||
const t = normType(n.type)
|
||||
if (t === 'CHANNEL') channelOrgId = n.id
|
||||
if (t === 'SERVICE_PROVIDER') agentOrgId = n.id
|
||||
}
|
||||
const last = path[path.length - 1]
|
||||
const orgId = last.id
|
||||
const lt = normType(last.type)
|
||||
if (lt === 'CHANNEL') {
|
||||
channelOrgId = last.id
|
||||
agentOrgId = null
|
||||
} else if (lt === 'SERVICE_PROVIDER') {
|
||||
agentOrgId = last.id
|
||||
}
|
||||
return { orgId, channelOrgId, agentOrgId }
|
||||
}
|
||||
|
||||
function onUpdateModelValue(val: string | number | null) {
|
||||
emit('update:modelValue', val)
|
||||
if (val == null || val === '') {
|
||||
emit('change', {
|
||||
node: null,
|
||||
path: [],
|
||||
orgId: null,
|
||||
channelOrgId: null,
|
||||
agentOrgId: null
|
||||
})
|
||||
return
|
||||
}
|
||||
const path = findPath(displayTree.value, val)
|
||||
const pathArr = path ?? []
|
||||
const node = pathArr.length ? pathArr[pathArr.length - 1] : null
|
||||
if (node) {
|
||||
emit('change', {
|
||||
node,
|
||||
path: pathArr,
|
||||
...deriveBindings(pathArr)
|
||||
})
|
||||
} else {
|
||||
emit('change', {
|
||||
node: null,
|
||||
path: [],
|
||||
orgId: val,
|
||||
channelOrgId: null,
|
||||
agentOrgId: null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function parseTreeResponse(data: any): OrgInfo[] {
|
||||
let list: any = []
|
||||
if (Array.isArray(data)) {
|
||||
list = data
|
||||
} else if (data && typeof data === 'object') {
|
||||
const d = data.data
|
||||
if (Array.isArray(d)) list = d
|
||||
else if (d && typeof d === 'object') list = d.tree || d.list || []
|
||||
else list = data.tree || data.list || []
|
||||
}
|
||||
if (!Array.isArray(list)) list = []
|
||||
if (list.length > 0 && list[0].children !== undefined) {
|
||||
return list as OrgInfo[]
|
||||
}
|
||||
return treeDataTranslate(list, 'id', 'parentId') as OrgInfo[]
|
||||
}
|
||||
|
||||
function loadTree(): Promise<void> {
|
||||
if (props.treeData != null && Array.isArray(props.treeData)) {
|
||||
rawTree.value = props.treeData
|
||||
emit('loaded', rawTree.value)
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (!props.autoLoad) {
|
||||
rawTree.value = []
|
||||
return Promise.resolve()
|
||||
}
|
||||
loading.value = true
|
||||
return getOrgTree()
|
||||
.then(({ data }) => {
|
||||
rawTree.value = Array.isArray(parseTreeResponse(data)) ? parseTreeResponse(data) : []
|
||||
emit('loaded', rawTree.value)
|
||||
})
|
||||
.catch(() => {
|
||||
rawTree.value = []
|
||||
emit('loaded', [])
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
if (props.autoLoad && props.treeData == null && rawTree.value.length === 0) {
|
||||
loadTree()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.treeData,
|
||||
(v) => {
|
||||
if (v != null && Array.isArray(v)) {
|
||||
rawTree.value = v
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
function refresh(): Promise<void> {
|
||||
rawTree.value = []
|
||||
return loadTree()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
loadTree,
|
||||
getDisplayTree: () => rawTree.value
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoLoad) {
|
||||
loadTree()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.org-tree-select-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
39
src/components/store-select/index.data.ts
Executable file
@@ -0,0 +1,39 @@
|
||||
import type { StoreInfo } from '@/api/store'
|
||||
|
||||
/** StoreSelect 组件 Props */
|
||||
export interface StoreSelectProps {
|
||||
/** 当前选中的值 */
|
||||
modelValue?: string | number
|
||||
/** 占位符文本 */
|
||||
placeholder?: string
|
||||
/** 是否可搜索 */
|
||||
filterable?: boolean
|
||||
/** 是否可清空 */
|
||||
clearable?: boolean
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean
|
||||
/** 选择器宽度 */
|
||||
width?: string
|
||||
/** 是否自动加载 */
|
||||
autoLoad?: boolean
|
||||
}
|
||||
|
||||
/** 门店选项 */
|
||||
export interface StoreOption {
|
||||
/** 门店 ID */
|
||||
id: string | number
|
||||
/** 门店名称 */
|
||||
name: string
|
||||
/** 原始数据 */
|
||||
raw?: StoreInfo
|
||||
}
|
||||
|
||||
/** StoreSelect 组件事件 */
|
||||
export interface StoreSelectEmits {
|
||||
/** 更新 modelValue */
|
||||
(e: 'update:modelValue', value: string | number): void
|
||||
/** 值改变时触发 */
|
||||
(e: 'change', value: string | number, selected: StoreOption | null): void
|
||||
/** 选项加载完成时触发 */
|
||||
(e: 'loaded', options: StoreOption[]): void
|
||||
}
|
||||
103
src/components/store-select/index.vue
Executable file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<el-select
|
||||
:model-value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:filterable="filterable"
|
||||
:clearable="clearable"
|
||||
:disabled="disabled"
|
||||
:loading="storeListLoading"
|
||||
:style="selectStyle"
|
||||
@update:model-value="onValueChange"
|
||||
@focus="loadStoreOptions"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in storeOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getStoreList } from '@/api/store'
|
||||
import type { StoreSelectProps, StoreOption } from './index.data'
|
||||
|
||||
const props = withDefaults(defineProps<StoreSelectProps>(), {
|
||||
modelValue: '',
|
||||
placeholder: '请选择门店',
|
||||
filterable: true,
|
||||
clearable: true,
|
||||
disabled: false,
|
||||
width: '100%',
|
||||
autoLoad: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | number]
|
||||
change: [value: string | number, selected: StoreOption | null]
|
||||
loaded: [options: StoreOption[]]
|
||||
}>()
|
||||
|
||||
const storeOptions = ref<StoreOption[]>([])
|
||||
const storeListLoading = ref(false)
|
||||
|
||||
const selectStyle = computed(() => ({
|
||||
width: props.width
|
||||
}))
|
||||
|
||||
function parseStoreListPayload(data: any): any[] {
|
||||
if (!data) return []
|
||||
const codeOk = data.code === 0 || data.code === undefined
|
||||
if (!codeOk) return []
|
||||
if (Array.isArray(data)) return data
|
||||
if (Array.isArray(data.list)) return data.list
|
||||
if (Array.isArray(data.data?.list)) return data.data.list
|
||||
if (Array.isArray(data.page?.list)) return data.page.list
|
||||
return []
|
||||
}
|
||||
|
||||
function loadStoreOptions() {
|
||||
if (storeOptions.value.length > 0) return
|
||||
storeListLoading.value = true
|
||||
getStoreList()
|
||||
.then(({ data }) => {
|
||||
const list = parseStoreListPayload(data)
|
||||
storeOptions.value = (list || []).map((item: any) => ({
|
||||
id: item.id ?? item.storeId ?? item.orgId ?? item.storeOrgId,
|
||||
name: item.name ?? item.storeName ?? item.orgName ?? '-',
|
||||
raw: item
|
||||
})).filter((item: StoreOption) => item.id != null)
|
||||
emit('loaded', storeOptions.value)
|
||||
})
|
||||
.catch(() => {
|
||||
storeOptions.value = []
|
||||
})
|
||||
.finally(() => {
|
||||
storeListLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onValueChange(val: string | number) {
|
||||
emit('update:modelValue', val)
|
||||
const selected = storeOptions.value.find(item => item.id === val)
|
||||
emit('change', val, selected ?? null)
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
storeOptions.value = []
|
||||
loadStoreOptions()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
loadStoreOptions
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoLoad) {
|
||||
loadStoreOptions()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
20
src/components/table-tree-column/index.data.ts
Executable file
@@ -0,0 +1,20 @@
|
||||
/** TableTreeColumn 组件 Props */
|
||||
export interface TableTreeColumnProps {
|
||||
/** 列属性名 */
|
||||
prop?: string
|
||||
/** 树节点的 key */
|
||||
treeKey?: string
|
||||
/** 父节点 key */
|
||||
parentKey?: string
|
||||
/** 层级 key */
|
||||
levelKey?: string
|
||||
/** 子节点 key */
|
||||
childKey?: string
|
||||
}
|
||||
|
||||
/** 表格行数据 */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export interface TableRow extends Record<string, any> {
|
||||
/** 是否展开 */
|
||||
_expanded?: boolean
|
||||
}
|
||||
87
src/components/table-tree-column/index.vue
Executable file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<el-table-column :prop="prop" v-bind="$attrs">
|
||||
<template #default="scope">
|
||||
<span @click.prevent="toggleHandle(scope.$index, scope.row)" :style="childStyles(scope.row)">
|
||||
<i :class="iconClasses(scope.row)" :style="iconStyles(scope.row)"></i>
|
||||
{{ scope.row[prop] }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, getCurrentInstance } from 'vue'
|
||||
import isArray from 'lodash/isArray'
|
||||
import type { TableTreeColumnProps, TableRow } from './index.data'
|
||||
|
||||
const props = withDefaults(defineProps<TableTreeColumnProps>(), {
|
||||
prop: '',
|
||||
treeKey: 'id',
|
||||
parentKey: 'parentId',
|
||||
levelKey: '_level',
|
||||
childKey: 'children'
|
||||
})
|
||||
|
||||
function childStyles(row: TableRow) {
|
||||
return { 'padding-left': (row[props.levelKey] > 1 ? row[props.levelKey] * 7 : 0) + 'px' }
|
||||
}
|
||||
|
||||
function iconClasses(row: TableRow) {
|
||||
return [!row._expanded ? 'el-icon-caret-right' : 'el-icon-caret-bottom']
|
||||
}
|
||||
|
||||
function iconStyles(row: TableRow): { visibility: 'visible' | 'hidden' } {
|
||||
return { visibility: hasChild(row) ? 'visible' : 'hidden' }
|
||||
}
|
||||
|
||||
function hasChild(row: TableRow) {
|
||||
return (isArray(row[props.childKey]) && row[props.childKey].length >= 1) || false
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function toggleHandle(index: number, row: TableRow) {
|
||||
if (hasChild(row)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const parentTable: any = (() => {
|
||||
// 在setup中访问父组件需要使用getCurrentInstance
|
||||
// 这里保留原有逻辑,通过$parent访问
|
||||
const instance = getCurrentInstance()
|
||||
return instance?.parent
|
||||
})()
|
||||
|
||||
// 使用更兼容的方式获取table数据
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const tableInstance = parentTable?.proxy?.$parent as any
|
||||
if (!tableInstance?.store?.states?.data) return
|
||||
|
||||
const data = tableInstance.store.states.data.slice(0)
|
||||
data[index]._expanded = !data[index]._expanded
|
||||
if (data[index]._expanded) {
|
||||
data.splice(0, index + 1).concat(row[props.childKey]).concat(data)
|
||||
} else {
|
||||
removeChildNode(data, row[props.treeKey])
|
||||
}
|
||||
tableInstance.store.commit('setData', data)
|
||||
nextTick(() => {
|
||||
tableInstance.doLayout()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function removeChildNode(data: TableRow[], parentId: string | number | (string | number)[]): TableRow[] {
|
||||
const parentIds = isArray(parentId) ? parentId : [parentId]
|
||||
if ((parentIds as (string | number)[]).length <= 0) {
|
||||
return data
|
||||
}
|
||||
const ids: (string | number)[] = []
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if ((parentIds as (string | number)[]).indexOf(data[i][props.parentKey]) !== -1 &&
|
||||
(parentIds as (string | number)[]).indexOf(data[i][props.treeKey]) === -1) {
|
||||
data[i]._expanded = false
|
||||
ids.push(data.splice(i, 1)[0][props.treeKey])
|
||||
i--
|
||||
}
|
||||
}
|
||||
return removeChildNode(data, ids)
|
||||
}
|
||||
</script>
|
||||
162
src/composables/useChannelScopeFromRole.ts
Executable file
@@ -0,0 +1,162 @@
|
||||
import { computed, nextTick, watch, type ComputedRef, type Ref } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getOrgInfo, type OrgInfo } from '@/api/org'
|
||||
|
||||
/**
|
||||
* useChannelScopeFromRole 选项
|
||||
*/
|
||||
export interface ChannelScopeOptions {
|
||||
channelId: Ref<string | number | null>
|
||||
agentOrgId: Ref<string | number | null>
|
||||
storeId: Ref<string | number | null>
|
||||
channelAgentTreeSelectRef: Ref<{ loadTree?: () => Promise<void> } | null>
|
||||
agentStoreSelectRef: Ref<{ loadStoreOptions?: () => Promise<void> } | null>
|
||||
}
|
||||
|
||||
/**
|
||||
* useChannelScopeFromRole 返回值
|
||||
*/
|
||||
export interface ChannelScopeReturn {
|
||||
showChannelPicker: ComputedRef<boolean>
|
||||
showAgentPicker: ComputedRef<boolean>
|
||||
ensureChannelFilterFromUserRole: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析组织信息响应
|
||||
* @param data - API 响应数据
|
||||
* @returns 组织信息或 null
|
||||
*/
|
||||
export function parseOrgInfoPayload(
|
||||
data:
|
||||
| {
|
||||
code?: number
|
||||
org?: OrgInfo
|
||||
data?: OrgInfo | null
|
||||
}
|
||||
| OrgInfo
|
||||
| null
|
||||
): OrgInfo | null {
|
||||
if (!data) return null
|
||||
if (typeof data === 'object' && 'code' in data && data.code !== undefined) {
|
||||
const p = data as { code?: number; org?: OrgInfo; data?: OrgInfo | null }
|
||||
if (p.code === 0 && p.org) return p.org
|
||||
if (p.code === 0 && p.data != null) return p.data
|
||||
return null
|
||||
}
|
||||
return data as OrgInfo
|
||||
}
|
||||
|
||||
/** 自某组织向上查找渠道组织 ID */
|
||||
export async function findChannelIdFromAncestor(orgId: number): Promise<number | null> {
|
||||
let cid: number | null = orgId
|
||||
let guard = 0
|
||||
while (cid != null && guard++ < 24) {
|
||||
const res = await getOrgInfo(cid)
|
||||
const info = parseOrgInfoPayload(res)
|
||||
if (!info) return null
|
||||
const t = String(info.type || '').toUpperCase()
|
||||
if (t === 'CHANNEL') return info.id
|
||||
cid = info.parentId ?? null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析渠道 ID,失败时短暂重试(避免首屏接口竞态)
|
||||
* @param orgId - 组织 ID
|
||||
* @param attempts - 重试次数
|
||||
* @param delayMs - 延迟毫秒数
|
||||
* @returns 渠道 ID 或 null
|
||||
*/
|
||||
async function findChannelIdFromAncestorWithRetry(
|
||||
orgId: number,
|
||||
attempts: number = 3,
|
||||
delayMs: number = 120
|
||||
): Promise<number | null> {
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
if (i > 0) await new Promise(r => setTimeout(r, delayMs))
|
||||
const ch = await findChannelIdFromAncestor(orgId)
|
||||
if (ch != null) return ch
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 渠道账号:不展示「渠道」选择框(showChannelPicker=false);纯代理商账号不展示「代理商」树(showAgentPicker=false)。
|
||||
* 内部仍会解析并写入 channelId;纯代理商登录时写入 agentOrgId=当前所属组织,供门店下拉与接口 org 范围使用。
|
||||
*/
|
||||
export function useChannelScopeFromRole({
|
||||
channelId,
|
||||
agentOrgId,
|
||||
storeId: _storeId,
|
||||
channelAgentTreeSelectRef,
|
||||
agentStoreSelectRef
|
||||
}: ChannelScopeOptions): ChannelScopeReturn {
|
||||
const userStore = useUserStore()
|
||||
/** 仅平台等非渠道/非代理商角色展示「渠道」下拉 */
|
||||
const showChannelPicker = computed(() => !userStore.isAgent && !userStore.isChannel)
|
||||
/**
|
||||
* 纯代理商:不展示「代理商」树(范围锁定当前组织)。
|
||||
* 含渠道角色时仍展示(即使角色名同时命中「代理」类关键字,避免树被误藏)。
|
||||
*/
|
||||
const showAgentPicker = computed(() => !userStore.isAgent || userStore.isChannel)
|
||||
|
||||
async function ensureChannelFilterFromUserRole(): Promise<void> {
|
||||
if (userStore.isChannel) {
|
||||
const oid = userStore.orgId
|
||||
if (oid == null) return
|
||||
if (channelId.value == null || channelId.value === '') {
|
||||
const res = await getOrgInfo(Number(oid))
|
||||
const info = parseOrgInfoPayload(res)
|
||||
const t = String(info?.type || '').toUpperCase()
|
||||
if (t === 'CHANNEL') {
|
||||
channelId.value = info?.id ?? oid
|
||||
} else {
|
||||
const ch = await findChannelIdFromAncestorWithRetry(Number(oid))
|
||||
if (ch != null) channelId.value = ch
|
||||
}
|
||||
}
|
||||
await nextTick()
|
||||
await channelAgentTreeSelectRef.value?.loadTree?.()
|
||||
await agentStoreSelectRef.value?.loadStoreOptions?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (!userStore.isAgent) return
|
||||
const oid = userStore.orgId
|
||||
if (oid == null) return
|
||||
|
||||
if (channelId.value == null || channelId.value === '') {
|
||||
const ch = await findChannelIdFromAncestorWithRetry(Number(oid))
|
||||
if (ch != null) {
|
||||
channelId.value = ch
|
||||
await nextTick()
|
||||
await channelAgentTreeSelectRef.value?.loadTree?.()
|
||||
}
|
||||
}
|
||||
|
||||
/** 代理商账号不可切换代理商:范围固定为当前登录组织 */
|
||||
agentOrgId.value = oid
|
||||
await nextTick()
|
||||
await agentStoreSelectRef.value?.loadStoreOptions?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* orgId / 角色晚于首屏就绪时补解析 channelId 并刷新代理商树、门店列表
|
||||
* (避免渠道账号首页等仅 onMounted 跑一次时 channelId 仍为空导致代理商下拉无数据)
|
||||
*/
|
||||
watch(
|
||||
() => [userStore.orgId, userStore.isAgent, userStore.isChannel] as const,
|
||||
async () => {
|
||||
if (!userStore.isAgent && !userStore.isChannel) return
|
||||
if (userStore.orgId == null) return
|
||||
if (channelId.value != null && channelId.value !== '') return
|
||||
await ensureChannelFilterFromUserRole()
|
||||
await agentStoreSelectRef.value?.loadStoreOptions?.()
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
return { showChannelPicker, showAgentPicker, ensureChannelFilterFromUserRole }
|
||||
}
|
||||
31
src/composables/useFlashScrollbar.ts
Executable file
@@ -0,0 +1,31 @@
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
|
||||
const DEFAULT_HIDE_AFTER_MS = 900
|
||||
|
||||
/** 滚动条默认隐藏,发生滚动后短暂显示,停止滚动一段时间后自动隐藏 */
|
||||
export function useFlashScrollbar(hideAfterMs = DEFAULT_HIDE_AFTER_MS) {
|
||||
const scrollbarVisible = ref(false)
|
||||
let hideTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function clearHideTimer() {
|
||||
if (hideTimer != null) {
|
||||
clearTimeout(hideTimer)
|
||||
hideTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function onScrollableScroll() {
|
||||
scrollbarVisible.value = true
|
||||
clearHideTimer()
|
||||
hideTimer = setTimeout(() => {
|
||||
scrollbarVisible.value = false
|
||||
hideTimer = null
|
||||
}, hideAfterMs)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
clearHideTimer()
|
||||
})
|
||||
|
||||
return { scrollbarVisible, onScrollableScroll }
|
||||
}
|
||||
69
src/composables/useNearExpiryUnusedCards.ts
Executable file
@@ -0,0 +1,69 @@
|
||||
import { ref } from 'vue'
|
||||
import { getNearExpiryUnusedCards } from '@/api/card'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
import type { NearExpiryUnusedCard } from '@/api/card'
|
||||
|
||||
export const NEAR_EXPIRY_UNUSED_PROMPT_SHOWN_KEY = 'nearExpiryUnusedCardsPromptShown'
|
||||
|
||||
type NearExpiryUnusedPayload = ApiResponse<{ list: NearExpiryUnusedCard[] }> & {
|
||||
list?: NearExpiryUnusedCard[]
|
||||
}
|
||||
|
||||
const nearExpiryUnusedList = ref<NearExpiryUnusedCard[]>([])
|
||||
const nearExpiryUnusedCount = ref(0)
|
||||
const nearExpiryLoading = ref(false)
|
||||
const nearExpiryDialogVisible = ref(false)
|
||||
|
||||
function parseNearExpiryList (body: NearExpiryUnusedPayload | null | undefined): NearExpiryUnusedCard[] {
|
||||
if (!body || body.code !== 0) return []
|
||||
return Array.isArray(body.list) ? body.list : []
|
||||
}
|
||||
|
||||
async function fetchNearExpiryUnusedCards (): Promise<NearExpiryUnusedCard[]> {
|
||||
nearExpiryLoading.value = true
|
||||
try {
|
||||
const body = await getNearExpiryUnusedCards()
|
||||
const list = parseNearExpiryList(body as NearExpiryUnusedPayload)
|
||||
nearExpiryUnusedList.value = list
|
||||
nearExpiryUnusedCount.value = list.length
|
||||
return list
|
||||
} catch {
|
||||
nearExpiryUnusedList.value = []
|
||||
nearExpiryUnusedCount.value = 0
|
||||
return []
|
||||
} finally {
|
||||
nearExpiryLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 登录后检查临期卡,存在则自动弹窗(同一会话仅自动弹一次) */
|
||||
export async function checkNearExpiryUnusedCardsAfterLogin (): Promise<void> {
|
||||
if (sessionStorage.getItem(NEAR_EXPIRY_UNUSED_PROMPT_SHOWN_KEY) === '1') return
|
||||
const list = await fetchNearExpiryUnusedCards()
|
||||
if (list.length > 0) {
|
||||
sessionStorage.setItem(NEAR_EXPIRY_UNUSED_PROMPT_SHOWN_KEY, '1')
|
||||
nearExpiryDialogVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
export function openNearExpiryUnusedCardsDialog (): void {
|
||||
nearExpiryDialogVisible.value = true
|
||||
void fetchNearExpiryUnusedCards()
|
||||
}
|
||||
|
||||
export function resetNearExpiryUnusedCardsPrompt (): void {
|
||||
sessionStorage.removeItem(NEAR_EXPIRY_UNUSED_PROMPT_SHOWN_KEY)
|
||||
}
|
||||
|
||||
export function useNearExpiryUnusedCards () {
|
||||
return {
|
||||
nearExpiryUnusedList,
|
||||
nearExpiryUnusedCount,
|
||||
nearExpiryLoading,
|
||||
nearExpiryDialogVisible,
|
||||
fetchNearExpiryUnusedCards,
|
||||
checkNearExpiryUnusedCardsAfterLogin,
|
||||
openNearExpiryUnusedCardsDialog,
|
||||
resetNearExpiryUnusedCardsPrompt
|
||||
}
|
||||
}
|
||||
1
src/config/env.d.ts
vendored
Executable file
@@ -0,0 +1 @@
|
||||
export const baseApi: string
|
||||
35
src/config/env.js
Executable file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 项目环境变量统一入口
|
||||
* 所有通过 Vite 注入的 VITE_ 变量在此集中定义默认值与类型说明,便于维护和 IDE 提示
|
||||
*
|
||||
* 使用方式:
|
||||
* import { baseApi, devPort, isDev } from '@/config/env'
|
||||
*
|
||||
* 配置方式:
|
||||
* 在项目根目录 .env / .env.development / .env.production 中配置,变量名需以 VITE_ 开头
|
||||
*/
|
||||
|
||||
const env = import.meta.env
|
||||
|
||||
/** 后端 API 基础地址(开发走 Vite 代理,生产走 nginx 代理,均为同源 /api) */
|
||||
export const baseApi = '/api'
|
||||
|
||||
/** 开发服务器端口 */
|
||||
export const devPort = Number(env.VITE_DEV_PORT) || 3000
|
||||
|
||||
/** 是否为开发环境 */
|
||||
export const isDev = env.DEV
|
||||
|
||||
/** 是否为生产环境 */
|
||||
export const isProd = env.PROD
|
||||
|
||||
/** 当前 mode:development | production */
|
||||
export const mode = env.MODE
|
||||
|
||||
export default {
|
||||
baseApi,
|
||||
devPort,
|
||||
isDev,
|
||||
isProd,
|
||||
mode
|
||||
}
|
||||
0
src/icons/iconfont.js
Executable file
10
src/icons/index.d.ts
vendored
Executable file
@@ -0,0 +1,10 @@
|
||||
import { App } from 'vue'
|
||||
import IconSvg from '@/components/icon-svg/index.vue'
|
||||
|
||||
export function getNameList(): string[]
|
||||
|
||||
export default {
|
||||
install(app: App): void
|
||||
}
|
||||
|
||||
export { IconSvg }
|
||||
17
src/icons/index.js
Executable file
@@ -0,0 +1,17 @@
|
||||
import IconSvg from '@/components/icon-svg/index.vue'
|
||||
import './iconfont.js'
|
||||
|
||||
const svgFiles = import.meta.glob('./svg/*.svg', { eager: true })
|
||||
const iconList = Object.values(svgFiles)
|
||||
|
||||
export function getNameList () {
|
||||
return iconList.map(item => item.default.id.replace('icon-', ''))
|
||||
}
|
||||
|
||||
export default {
|
||||
install (app) {
|
||||
app.component('IconSvg', IconSvg)
|
||||
}
|
||||
}
|
||||
|
||||
export { IconSvg }
|
||||
1
src/icons/svg/icon-admin.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1525499040598" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1423" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M491.392 286.464m-286.464 0a4.476 4.476 0 1 0 572.928 0 4.476 4.476 0 1 0-572.928 0Z" p-id="1424"></path><path d="M869.312 523.264c-35.392-43.2-101.888-51.2-140.16-10.496-59.648 63.36-143.744 103.232-237.824 103.232-83.264 0-158.976-31.04-216.832-82.048C230.656 495.296 161.856 510.592 129.024 558.976c-49.28 72.768-78.4 160.064-78.4 254.72 0 19.072 1.28 37.76 3.648 56.064C66.176 960.32 150.272 1024 241.536 1024l539.392 0c100.544 0 183.744-77.824 191.232-178.048 0.768-10.624 1.216-21.376 1.216-32.192C973.376 703.296 934.208 602.432 869.312 523.264z" p-id="1425"></path></svg>
|
||||
|
After Width: | Height: | Size: 965 B |
1
src/icons/svg/icon-bianji.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1523793686319" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2491" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M908.785955 492.415966c-11.04966 0-20.008706 8.958022-20.008706 20.008706l0 338.328565c0 38.614459-31.416524 70.030983-70.030983 70.030983L202.707748 920.78422c-38.614459 0-70.030983-31.416524-70.030983-70.030983l0-676.303067c0-38.614459 31.416524-70.030983 70.030983-70.030983l362.378308 0c11.04966 0 20.008706-8.958022 20.008706-20.008706 0-11.050684-8.959046-20.008706-20.008706-20.008706L202.707748 64.401776c-60.681034 0-110.049418 49.367361-110.049418 110.049418l0 676.303067c0 60.681034 49.367361 110.049418 110.049418 110.049418l616.038518 0c60.681034 0 110.049418-49.367361 110.049418-110.049418L928.795685 512.425695C928.795685 501.373988 919.836639 492.415966 908.785955 492.415966z" p-id="2492"></path><path d="M942.576549 117.985158 907.20597 82.614579c-23.405059-23.405059-61.486376-23.400966-84.891436 0l-422.369633 422.36861c-1.295506 1.296529-2.407839 2.76395-3.304256 4.363378L269.318842 736.416166c-4.457522 7.951089-2.977821 17.909905 3.598968 24.221658 3.830235 3.675716 8.823969 5.572926 13.859659 5.572926 3.613294 0 7.247054-0.977257 10.488889-2.973728l219.285309-135.104047c1.327228-0.816598 2.552126-1.784646 3.654226-2.885723l422.369633-422.36861C965.979562 179.471534 965.979562 141.390217 942.576549 117.985158zM418.556794 552.080857l55.42124 55.279001-132.025942 81.343633L418.556794 552.080857zM914.279063 174.579107 505.926678 582.930469l-63.748906-63.586201L850.612021 110.912066c7.798617-7.800663 20.493753-7.804756 28.29544 0l35.371602 35.371602C922.081773 154.084331 922.081773 166.777421 914.279063 174.579107z" p-id="2493"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
src/icons/svg/icon-config.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1525499385323" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4161" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M993.889598 704.576038l-41.044193 12.788439c1.179981 8.865801 2.455635 17.508362 2.455636 26.661186 0 9.057149-1.435112 17.859167-2.806441 26.533619l41.394998 12.884114c9.694976 2.806441 17.859167 9.376063 22.57909 18.241864 4.688031 8.865801 5.644772 19.262387 2.423745 28.829798-6.665296 19.708867-28.574668 30.360584-49.080819 24.046093l-41.873369-12.979788a207.389579 207.389579 0 0 1-32.178392 43.27659l25.544987 33.741069a36.388053 36.388053 0 0 1-8.61067 52.174281 39.896104 39.896104 0 0 1-54.438568-8.227973l-25.672553-33.900526a218.009405 218.009405 0 0 1-52.748326 17.093774v41.139867c0 20.601825-17.476471 37.408577-38.939363 37.408577-21.558566 0-39.003146-16.711078-39.003145-37.408577v-41.139867a217.818057 217.818057 0 0 1-52.780218-17.093774l-25.60877 33.900526a40.183126 40.183126 0 0 1-54.47046 8.227973 36.164814 36.164814 0 0 1-8.578778-52.174281l25.800118-34.059983a201.138871 201.138871 0 0 1-33.29459-42.702545l-40.916628 12.78844c-20.601825 6.442057-42.511196-4.337226-49.11271-24.046093a36.930206 36.930206 0 0 1 24.970943-47.071662l41.076084-12.724657c-1.148089-8.833909-2.519418-17.604036-2.519418-26.693077 0-9.184715 1.498894-17.859167 2.838332-26.661185l-41.42689-12.820331c-20.569934-6.442057-31.66813-27.490361-24.970942-47.135445 6.601514-19.613193 28.510885-30.296802 49.112709-24.014201l41.841478 13.04357a211.057087 211.057087 0 0 1 32.178392-43.372263l-25.513096-33.645396a36.451836 36.451836 0 0 1 8.578779-52.206172 39.959887 39.959887 0 0 1 54.47046 8.291756l25.640661 33.900526a214.469463 214.469463 0 0 1 52.748326-17.125665v-41.139868c0-20.633716 17.412688-37.408577 39.003146-37.408577 21.494783 0 38.971254 16.647295 38.971254 37.408577v41.076085a216.733751 216.733751 0 0 1 52.716435 17.157557l25.672553-33.932418a40.119343 40.119343 0 0 1 54.438568-8.291756c17.348905 12.182503 21.271544 35.495095 8.642561 52.238064l-25.832009 34.028092c12.724657 12.916005 24.365007 26.980099 33.358373 42.60687l40.884736-12.660874c20.538042-6.314491 42.415522 4.401009 49.080819 24.077985 6.76097 19.613193-4.528575 40.661497-25.002835 47.071662z m-252.898564-91.273101c-75.327416 0-136.463172 58.616338-136.463172 130.8184 0 72.265844 61.135756 130.754617 136.463172 130.754617 75.231742 0 136.39939-58.488773 136.39939-130.754617 0-72.202062-61.071974-130.8184-136.39939-130.8184z" p-id="4162"></path><path d="M441.690527 895.669127H99.528018a49.750537 49.750537 0 0 1-49.782429-49.782429V99.596749c0-27.45847 22.292068-49.782429 49.782429-49.782429h447.818617v223.909309a99.532966 99.532966 0 0 0 99.532966 99.532966h199.002149v36.451836a322.102837 322.102837 0 0 1 49.782429 32.018935V348.381326l-8.802018-8.770127 1.530785-8.802018-307.05011-330.713507H99.559909A99.532966 99.532966 0 0 0 0.026943 99.62864v746.321841a99.532966 99.532966 0 0 0 99.532966 99.532966h390.446043a322.963904 322.963904 0 0 1-48.315425-49.81432z" p-id="4163"></path><path d="M597.129064 91.145536l217.818057 232.32863h-168.06752a49.750537 49.750537 0 0 1-49.782428-49.750537V91.145536h0.031891zM174.185715 423.007132h248.784577v-49.750537H174.185715v49.750537zM422.970292 223.9412H174.185715v49.782429h248.784577V223.9412zM174.185715 744.057554h248.784577v-49.750537H174.185715v49.750537zM514.115828 545.023514H174.185715v49.750537h339.930113v-49.750537z" p-id="4164"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
1
src/icons/svg/icon-dangdifill.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1523793636434" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1860" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M814.898635 729.554898c-52.181453-15.880689-113.718995-27.296693-179.771363-33.619702 83.378989-122.791627 144.990209-275.473005 144.990209-379.626133 0-144.027278-119.42802-260.733303-266.739087-260.733303-147.317207 0-266.739087 116.706025-266.739087 260.733303 0 102.385878 61.574381 255.895111 145.227616 379.482871-66.636676 6.290263-128.727827 17.752316-181.310416 33.752732-60.22771 18.326391-132.022872 49.885154-132.022872 96.160083 0 89.684602 218.426755 136.608308 434.19496 136.608308 112.413256 0 218.348983-12.394285 298.29478-34.899858 88.904843-25.027 135.898133-60.19701 135.898133-101.707426C946.922531 779.442098 875.128391 747.883335 814.898635 729.554898zM514.324952 181.990914c60.772109 0 110.043279 49.269124 110.043279 110.063745 0 60.753689-49.27117 110.034069-110.043279 110.034069-60.784388 0-110.054535-49.28038-110.054535-110.034069C404.269393 231.260038 453.540563 181.990914 514.324952 181.990914zM883.279025 854.435093c-18.848277 12.544711-47.583738 24.460088-83.099626 34.458813-76.514651 21.538549-178.599677 33.401738-287.450804 33.401738s-210.9382-11.863189-287.451828-33.401738c-35.515888-9.998725-64.250326-21.914102-83.099626-34.458813-15.234983-10.140964-23.626094-20.343327-23.626094-28.729322 0-5.562692 17.591657-31.6877 103.654778-57.875129 56.150858-17.086144 123.93773-28.755927 196.708104-34.169217 24.391527 32.31601 50.161447 61.676711 76.472696 86.332251 4.39203 4.923126 10.781554 8.099469 17.99177 8.099469 7.100722 0 13.407358-3.080152 17.799388-7.869225 26.40744-24.667819 52.311413-54.06229 76.838016-86.449931 72.206532 5.462408 139.457192 17.093307 195.23352 34.06791 86.064145 26.191522 103.655801 52.306296 103.655801 57.865919C906.904095 834.091766 898.514008 844.294129 883.279025 854.435093z" p-id="1861"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
1
src/icons/svg/icon-daohang.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1523793771421" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3764" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M605.848435 958.708971c-0.36225 0-0.724501-0.008186-1.088798-0.026606-9.098215-0.468674-16.872273-6.712889-19.288299-15.495926L476.898551 548.458353 80.07064 437.83486c-8.760524-2.442632-14.976086-10.218736-15.427364-19.304671-0.451278-9.083889 4.965082-17.437138 13.44215-20.73423L929.251056 66.728774c7.80885-3.038196 16.669658-1.174756 22.597671 4.750187 5.922896 5.92392 7.788383 14.789844 4.751211 22.597671L625.531729 945.241238C622.361527 953.390849 614.518908 958.708971 605.848435 958.708971zM152.537092 414.172951l347.232352 96.79658c7.148817 1.9934 12.726859 7.591909 14.696724 14.746866l94.821599 344.730369 290.525839-746.93166L152.537092 414.172951z" p-id="3765"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
src/icons/svg/icon-duanxin.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1523793786368" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4016" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M958.359001 286.941198c0-64.308655-52.132334-116.440989-116.440989-116.440989L181.453678 170.500209c-64.308655 0-116.440989 52.132334-116.440989 116.440989l0 448.63995c0 64.308655 52.132334 116.440989 116.440989 116.440989l660.464333 0c64.308655 0 116.440989-52.132334 116.440989-116.440989L958.359001 286.941198zM843.814198 212.455763c12.059664 0 23.444968 3.465938 33.519418 8.598842L530.749016 549.347606c-1.227967 1.161453-1.156336 0.916882-1.752924 1.868557-1.426489 1.067308-6.055926 3.922333-15.449877 3.922333-9.397021 0-14.025435-2.75474-15.4509-3.818979-0.676405-1.077541-0.662079-0.716314-1.995447-1.940189l-353.122503-324.412624c11.609409-7.557116 25.445532-12.509918 40.300868-12.509918L843.814198 212.456786zM916.403446 736.484727c0 40.857547-31.730679 73.582879-72.589248 73.582879L183.27721 810.067606c-40.85857 0-75.28566-32.725332-75.28566-73.582879L107.99155 287.657512c0-10.630105 2.860141-20.735253 6.909363-29.882588l351.486236 322.096882c4.91187 5.302773 19.479657 17.676591 47.009663 17.676591 27.799136 0 42.303478-12.618389 47.076178-17.830087l346.800517-328.899822c6.251378 10.860349 9.129938 23.433712 9.129938 36.838L916.403446 736.484727z" p-id="4017"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
src/icons/svg/icon-editor.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1527128057551" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1399" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M218.316 307.727h87.886v205.06h-29.297v29.295h117.179v-29.294H364.79V307.727h87.882v29.293h29.294v-87.882H189.022v87.882h29.294v-29.293z m322.242 58.59h292.945v58.588H540.558v-58.588z m0 117.177h292.945v58.588H540.558v-58.588z m-351.536 117.18h644.481v58.588h-644.48v-58.587z m0 117.176h644.481v58.588h-644.48V717.85z m351.536-468.713h292.945v58.589H540.558v-58.589z m420.923 713.13H61.045V63.309h900.436v898.958z m-864.62-35.816h828.804V99.125H96.861V926.45z" p-id="1400"></path></svg>
|
||||
|
After Width: | Height: | Size: 872 B |
1
src/icons/svg/icon-geren.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1523793764144" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3637" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M586.945923 513.581008c55.067176-27.962865 92.91211-85.125773 92.91211-150.998039 0-93.338828-75.937506-169.276335-169.277358-169.276335s-169.275311 75.937506-169.275311 169.276335c0 65.872267 37.844933 123.034151 92.911086 150.998039-95.652524 32.016181-164.778904 122.45496-164.778904 228.743728 0 11.31572 9.17394 20.491707 20.491707 20.491707s20.491707-9.174963 20.491707-20.491707c0-110.36869 89.791026-200.160739 200.160739-200.160739S710.741413 631.956046 710.741413 742.324736c0 11.31572 9.17394 20.491707 20.491707 20.491707s20.491707-9.174963 20.491707-20.491707C751.723803 636.035968 682.598446 545.598212 586.945923 513.581008zM382.287753 362.582969c0-70.742181 57.552787-128.293945 128.292921-128.293945 70.742181 0 128.293945 57.552787 128.293945 128.293945 0 70.741157-57.552787 128.292921-128.293945 128.292921C439.84054 490.876913 382.287753 433.324126 382.287753 362.582969z" p-id="3638"></path><path d="M827.871087 196.127889C743.498468 111.757317 631.320573 65.290005 512 65.290005S280.500509 111.756293 196.128913 196.127889C111.756293 280.501532 65.291029 392.678404 65.291029 511.998977s46.465265 231.499491 130.837884 315.872111 196.550515 130.837884 315.871087 130.837884 231.498468-46.465265 315.871087-130.837884S958.708971 631.319549 958.708971 511.998977 912.243707 280.500509 827.871087 196.127889zM512 917.726581c-223.718271 0-405.726581-182.007287-405.726581-405.727605 0-223.718271 182.00831-405.726581 405.726581-405.726581s405.726581 182.007287 405.726581 405.726581C917.726581 735.719294 735.718271 917.726581 512 917.726581z" p-id="3639"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
src/icons/svg/icon-jiesuo.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1523793740417" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3258" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M753.845117 371.674021l-17.46272 0 0-83.669608c0-59.275012-22.62837-115.203812-63.715137-157.482731-42.170448-43.394323-99.369172-67.291592-161.058163-67.291592-126.040624 0-224.772276 98.731652-224.772276 224.7733l0 83.669608-16.680914 0c-62.788022 0-113.688295 50.900274-113.688295 113.688295L156.467611 842.961784c0 62.788022 50.900274 113.688295 113.688295 113.688295l483.690234 0c62.788022 0 113.688295-50.900274 113.688295-113.688295L867.534436 485.362316C867.532389 422.574295 816.633139 371.674021 753.845117 371.674021zM328.176344 288.005436c0-102.858646 80.573083-183.432753 183.431729-183.432753 50.423413 0 97.093339 19.447934 131.410935 54.762231 33.547047 34.519188 52.021817 80.214926 52.021817 128.670521l0 83.669608L328.176344 371.675044 328.176344 288.005436zM826.191842 842.961784c0 39.956014-32.390711 72.346725-72.346725 72.346725L270.154883 915.308509c-39.956014 0-72.346725-32.390711-72.346725-72.346725L197.808158 485.362316c0-39.956014 32.390711-72.346725 72.346725-72.346725l483.690234 0c39.956014 0 72.346725 32.390711 72.346725 72.346725L826.191842 842.961784z" p-id="3259"></path><path d="M509.932921 580.446905c-11.416004 0-20.670785 9.254781-20.670785 20.670785l0 109.554138c0 11.414981 9.254781 20.670785 20.670785 20.670785 11.416004 0 20.670785-9.254781 20.670785-20.670785L530.603707 601.116667C530.602683 589.701686 521.348925 580.446905 509.932921 580.446905z" p-id="3260"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
src/icons/svg/icon-job.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1525499354423" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4001" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M512 0C230.4 0 0 230.4 0 512s230.4 512 512 512 512-230.4 512-512-230.4-512-512-512z m0 960c-249.6 0-448-198.4-448-448s198.4-448 448-448 448 198.4 448 448-198.4 448-448 448z" fill="" p-id="4002"></path><path d="M556.8 518.4V339.2c0-19.2-12.8-32-32-32s-32 12.8-32 32v192c0 6.4 0 19.2 6.4 19.2l198.4 204.8c6.4 6.4 12.8 12.8 25.6 12.8 6.4 0 12.8 0 19.2-6.4 12.8-12.8 12.8-32 0-44.8L556.8 518.4z" fill="" p-id="4003"></path></svg>
|
||||
|
After Width: | Height: | Size: 811 B |
1
src/icons/svg/icon-log.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1525499440059" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4865" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M904 238.5L746.2 84.2c-13.1-13.1-31-20-48.9-20H195.8c-53.7 0-97.1 44.1-97.1 97.8v700c0 53.7 43.4 97.8 97.1 97.8h631.7c53.7 0 97.8-44.1 97.8-97.8V288.8c0-18.6-8.2-37.2-21.3-50.3zM856.4 862c0 15.8-13.1 28.9-28.9 28.9H195.8c-15.8 0-28.2-13.1-28.2-28.9V162c0-15.8 12.4-28.9 28.2-28.9l454 0.7v121.9c0 57.2 46.2 103.3 103.3 103.3h103.3v503z m0-571.8H753.1c-19.3 0-34.4-15.2-34.4-34.4v-102l137.8 135v1.4z" p-id="4866"></path><path d="M684 475.2H339.5c-19 0-34.4 15.4-34.4 34.4s15.4 34.4 34.4 34.4H684c19 0 34.4-15.4 34.4-34.4S703 475.2 684 475.2zM684 681.9H339.5c-19 0-34.4 15.4-34.4 34.4s15.4 34.4 34.4 34.4H684c19 0 34.4-15.4 34.4-34.4S703 681.9 684 681.9z" p-id="4867"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
src/icons/svg/icon-menu.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1525499228106" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2822" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M63.97608 127.975696v127.909182h895.454322V127.975696H63.97608z m255.843946 447.727161h639.610376V447.787536H319.820026v127.915321z m0 319.780118h639.610376V767.541047H319.820026v127.941928zM63.97608 575.702857h127.922485V447.787536H63.97608v127.915321z m0 319.780118h127.922485V767.541047H63.97608v127.941928z" p-id="2823"></path></svg>
|
||||
|
After Width: | Height: | Size: 723 B |
1
src/icons/svg/icon-mudedi.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1523793755611" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3511" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M954.382433 263.833921l-82.627882-98.120738c-3.925403-4.66116-9.707083-7.351432-15.801894-7.351432L577.082274 158.361751 577.082274 87.082334c0-11.408841-9.248641-20.657482-20.657482-20.657482l-86.759993 0c-11.408841 0-20.657482 9.248641-20.657482 20.657482L449.007317 271.967158 171.158195 271.967158c-6.327102 0-12.305257 2.90005-16.223496 7.868201L70.252017 387.261021c-5.91164 7.500834-5.91164 18.076704 0 25.577538l84.682681 107.423615c3.917216 4.970198 9.895371 7.869225 16.223496 7.869225l277.850146 0 0 299.32218-135.281079 0c-11.408841 0-20.657482 9.248641-20.657482 20.657482l0 86.759993c0 11.408841 9.248641 20.657482 20.657482 20.657482l396.616086 0c11.408841 0 20.657482-9.248641 20.657482-20.657482l0-86.759993c0-11.408841-9.248641-20.657482-20.657482-20.657482L577.082274 827.453578 577.082274 669.675112l159.40757 0c5.16872 0 10.149151-1.936095 13.959943-5.430686l76.081793-69.744457c4.266164-3.911076 6.697539-9.43693 6.697539-15.226796s-2.430352-11.314697-6.699586-15.226796l-76.081793-69.742411c-3.808746-3.493567-8.789177-5.430686-13.957897-5.430686L577.082274 488.87328l0-92.957135 278.869359 0c6.093788 0 11.875468-2.690272 15.801894-7.351432l82.627882-98.120738C960.855868 282.757922 960.855868 271.523043 954.382433 263.833921zM181.178409 486.817458l-68.399833-86.768179 68.399833-86.769202 267.829931 0 0 173.536358L181.178409 486.816434zM689.686888 914.214595l-355.302145 0 0-45.446052 355.302145 0L689.686888 914.214595zM490.322281 827.454602 490.322281 107.738793l45.446052 0 0 719.715809L490.322281 827.454602zM728.454844 530.189268l53.547567 49.084928-53.547567 49.086975L577.082274 628.361171l0-98.171903L728.454844 530.189268zM846.341764 354.60425 577.082274 354.60425l0-154.928558 269.25949 0 65.233724 77.464279L846.341764 354.60425z" p-id="3512"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
1
src/icons/svg/icon-oss.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1525499891937" class="icon" style="" viewBox="0 0 1291 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1508" xmlns:xlink="http://www.w3.org/1999/xlink" width="252.1484375" height="200"><defs><style type="text/css"></style></defs><path d="M779.656047 669.379378a31.50762 31.50762 0 0 1-22.37041-9.294747L645.906202 548.705195l-111.379436 111.379436a31.50762 31.50762 0 0 1-44.583281-44.583282l133.749845-133.592307a31.50762 31.50762 0 0 1 44.425744 0l133.749845 133.592307a31.50762 31.50762 0 0 1-22.212872 53.878029z" fill="" p-id="1509"></path><path d="M960.982398 1023.997637H787.69049a31.50762 31.50762 0 0 1 0-63.015239h166.360231a29.302086 29.302086 0 0 1 5.356296 0 283.568576 283.568576 0 0 0 54.980796-557.527329 31.50762 31.50762 0 0 1-23.630715-27.411629 346.583816 346.583816 0 0 0-689.701793 0 31.50762 31.50762 0 0 1-23.788253 27.411629A283.568576 283.568576 0 0 0 332.405387 960.982398a31.50762 31.50762 0 0 1 5.513833 0H645.906202a31.50762 31.50762 0 0 1 0 63.015239H330.830006a33.398077 33.398077 0 0 1-8.979672-1.260305 346.583816 346.583816 0 0 1-80.817044-675.05075 409.599055 409.599055 0 0 1 809.745824 0 346.583816 346.583816 0 0 1-81.132121 675.208288A31.50762 31.50762 0 0 1 960.982398 1023.997637z" fill="" p-id="1510"></path><path d="M645.906202 1008.243827a31.50762 31.50762 0 0 1-31.50762-31.507619V551.383343a31.50762 31.50762 0 0 1 63.015239 0v425.352865a31.50762 31.50762 0 0 1-31.507619 31.507619z" fill="" p-id="1511"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
src/icons/svg/icon-pinglun.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1523793693782" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2618" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M850.879104 96.41591l-676.303067 0c-60.681034 0-110.049418 49.367361-110.049418 110.049418l0 446.200388c0 60.681034 49.367361 110.049418 110.049418 110.049418l90.307795 0L396.936381 931.129846c3.793396 4.838192 9.598612 7.66354 15.746636 7.66354s11.952216-2.825348 15.746636-7.66354l132.052548-168.414711 290.396903 0c60.681034 0 110.049418-49.367361 110.049418-110.049418L960.928522 206.465329C960.928522 145.784294 911.561162 96.41591 850.879104 96.41591zM920.91111 652.665717c0 38.614459-31.416524 70.030983-70.030983 70.030983L550.744419 722.6967c-6.147 0-11.952216 2.825348-15.745612 7.66354L412.683017 886.356107l-122.31579-155.995867c-3.792373-4.838192-9.597589-7.66354-15.745612-7.66354l-100.045577 0c-38.614459 0-70.030983-31.416524-70.030983-70.030983L104.545054 206.465329c0-38.614459 31.416524-70.030983 70.030983-70.030983l676.303067 0c38.614459 0 70.030983 31.416524 70.030983 70.030983L920.910087 652.665717z" p-id="2619"></path><path d="M272.621051 344.526731c-44.132126 0-80.035848 35.903721-80.035848 80.035848 0 44.132126 35.903721 80.036871 80.035848 80.036871s80.035848-35.904745 80.035848-80.036871C352.655875 380.430452 316.752154 344.526731 272.621051 344.526731zM272.621051 464.582037c-22.065552 0-40.017412-17.951861-40.017412-40.018436 0-22.065552 17.952884-40.017412 40.017412-40.017412 22.065552 0 40.017412 17.951861 40.017412 40.017412C312.638463 446.629153 294.686602 464.582037 272.621051 464.582037z" p-id="2620"></path><path d="M512.727571 344.526731c-44.132126 0-80.035848 35.903721-80.035848 80.035848 0 44.132126 35.903721 80.036871 80.035848 80.036871 44.132126 0 80.035848-35.904745 80.035848-80.036871C592.763418 380.430452 556.859697 344.526731 512.727571 344.526731zM512.727571 464.582037c-22.065552 0-40.017412-17.951861-40.017412-40.018436 0-22.065552 17.951861-40.017412 40.017412-40.017412 22.065552 0 40.017412 17.951861 40.017412 40.017412C552.746006 446.629153 534.793122 464.582037 512.727571 464.582037z" p-id="2621"></path><path d="M752.836137 344.526731c-44.131103 0-80.035848 35.903721-80.035848 80.035848 0 44.132126 35.904745 80.036871 80.035848 80.036871s80.035848-35.904745 80.035848-80.036871C832.871985 380.430452 796.96724 344.526731 752.836137 344.526731zM752.836137 464.582037c-22.066575 0-40.017412-17.951861-40.017412-40.018436 0-22.065552 17.951861-40.017412 40.017412-40.017412s40.017412 17.951861 40.017412 40.017412C792.853549 446.629153 774.902712 464.582037 752.836137 464.582037z" p-id="2622"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
src/icons/svg/icon-role.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1525499185011" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2162" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M902.068357 744.573319c0-60.965821-71.69129-136.044101-128.705623-144.511577l77.336273-127.576626L671.753032 472.485116c32.176406-46.288864 53.627343-107.254686 53.627343-174.994487 0-5.080485 0-10.725469 0-15.805954 69.433297-12.418964 114.593164-32.176406 114.593164-51.933848 0-21.450937-50.240353-41.208379-127.012128-53.627343-3.38699-18.063947-55.320838-164.269019-116.286659-164.269019-27.095921 0-48.546858 14.112459-62.659316 35.563396l0 0c0 10.725469-10.725469 21.450937-24.837927 21.450937-12.418964 0-23.144432-9.031974-24.837927-19.757442l0 0C471.920617 27.660419 448.776185 11.289967 421.680265 11.289967 362.407938 11.289967 307.0871 145.640573 294.668137 179.510474 228.62183 191.929438 185.719956 209.993385 185.719956 231.444322s44.595369 39.514884 114.593164 51.933848c0 5.080485 0 10.725469 0 15.805954 0 67.739802 19.757442 128.705623 53.627343 174.994487L174.994487 474.178611l76.771775 125.318633c-57.014333 0-132.092613 78.46527-132.092613 146.76957l1.693495 205.477398c0 33.869901 26.531422 60.965821 60.965821 60.965821l661.027563 0c33.869901 0 60.965821-27.095921 60.965821-60.965821L902.068357 744.573319zM711.267916 176.123484 711.267916 176.123484 711.267916 176.123484 711.267916 176.123484zM464.582139 962.46968l-28.789416 0-53.627343-395.148842 112.335171 98.222712L464.582139 962.46968zM587.642778 962.46968 558.853363 962.46968l-30.482911-296.92613 112.899669-99.916207L587.642778 962.46968zM655.38258 338.699008l0 12.418964-14.112459 0c0 0-9.031974 76.771775-57.014333 76.771775-48.546858 0-57.578831-76.771775-57.578831-76.771775l-32.176406 0c-7.338479 62.659316-39.514884 76.771775-57.014333 76.771775-42.901874 0-55.320838-57.014333-57.014333-76.771775l-14.676957 0L365.794928 338.699008l0-12.418964 107.254686 0L473.049614 338.699008l71.69129 0 0-12.418964 107.254686 0L651.99559 338.699008 655.38258 338.699008z" p-id="2163"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
src/icons/svg/icon-shanchu.svg
Executable file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1523793725342" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3129" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M909.050991 169.476903l-217.554898 0 0-31.346939c0-39.5866-32.205493-71.792093-71.793116-71.792093L408.15591 66.337871c-39.5866 0-71.792093 32.205493-71.792093 71.792093l0 31.346939L113.349581 169.476903c-11.013845 0-19.942191 8.940626-19.942191 19.954471s8.928347 19.954471 19.942191 19.954471l84.264149 0 0 640.687918c0 60.479443 49.203632 109.683075 109.683075 109.683075l416.474366 0c60.479443 0 109.683075-49.203632 109.683075-109.683075L833.454246 209.385844l75.595722 0c11.012821 0 19.942191-8.940626 19.942191-19.954471S920.063813 169.476903 909.050991 169.476903zM376.2482 138.130987c0-17.593703 14.314007-31.907711 31.907711-31.907711l211.547067 0c17.593703 0 31.907711 14.314007 31.907711 31.907711l0 31.346939L376.2482 169.477926 376.2482 138.130987zM793.569864 850.074785c0 38.486546-31.312146 69.798692-69.798692 69.798692L307.297828 919.873478c-38.486546 0-69.798692-31.312146-69.798692-69.798692L237.499136 211.042577l556.070728 0L793.569864 850.074785z" p-id="3130"></path><path d="M510.662539 861.276918c11.012821 0 19.954471-8.92937 19.954471-19.942191L530.61701 294.912753c0-11.013845-8.94165-19.942191-19.954471-19.942191s-19.954471 8.928347-19.954471 19.942191L490.708068 841.334727C490.708068 852.347548 499.649717 861.276918 510.662539 861.276918z" p-id="3131"></path><path d="M374.562814 801.449321c11.012821 0 19.954471-8.92937 19.954471-19.942191L394.517285 354.74035c0-11.013845-8.94165-19.942191-19.954471-19.942191s-19.954471 8.928347-19.954471 19.942191l0 426.76678C354.608344 792.519951 363.549993 801.449321 374.562814 801.449321z" p-id="3132"></path><path d="M649.832182 801.449321c11.012821 0 19.954471-8.92937 19.954471-19.942191L669.786653 354.74035c0-11.013845-8.94165-19.942191-19.954471-19.942191s-19.954471 8.928347-19.954471 19.942191l0 426.76678C629.877711 792.519951 638.81936 801.449321 649.832182 801.449321z" p-id="3133"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |