init: 橘视厅酒店版会员系统前端

This commit is contained in:
zhaogd
2026-07-26 12:54:46 +08:00
commit 690568861b
243 changed files with 42311 additions and 0 deletions

19
.babelrc Executable file
View 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"]
}
}
}

View 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
View 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
View 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
View File

@@ -0,0 +1,3 @@
# 所有环境通用(可被 .env.development / .env.production 覆盖)
# 开发服务器端口(可选)
# VITE_DEV_PORT=3000

5
.env.development Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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']
}
}

View 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
View 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
View 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;

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

18
src/App.vue Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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'
})
}

View 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
View 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)
}

View 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
View 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
View 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
View 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'

View 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
View 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)
}

View 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
View 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)
}

View 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
View 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)
}

View 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)
}

View 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)
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
src/assets/img/login_bg.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

441
src/assets/scss/_base.scss Executable file
View 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
View 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
View 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
View File

@@ -0,0 +1,4 @@
// Dart Sass 推荐使用 @use 替代已弃用的 @import
@use "./normalize";
@use "./variables";
@use "./base";

View 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>

View 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
}

View 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>

View 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: '门店'
}

View 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>

View 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[]
}

View 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>

View 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
}

View 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>

View File

@@ -0,0 +1,11 @@
/** IconSvg 组件 Props */
export interface IconSvgProps {
/** 图标名称 */
name: string
/** 自定义类名 */
className?: string
/** 图标宽度 */
width?: string
/** 图标高度 */
height?: string
}

View 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>

View 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[]
}

View 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>

View 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
}

View 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>

View 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
}

View 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>

View 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 }
}

View 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 }
}

View 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
View File

@@ -0,0 +1 @@
export const baseApi: string

35
src/config/env.js Executable file
View 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
View File

10
src/icons/index.d.ts vendored Executable file
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Some files were not shown because too many files have changed in this diff Show More