Compare commits
No commits in common. "b63306890dde3eda7e163df704b62be3b881f9ad" and "2b4b9a2e9c78d71b2b8d9a35ab9a0bed540b3b79" have entirely different histories.
b63306890d
...
2b4b9a2e9c
23
AGENTS.md
23
AGENTS.md
@ -1,23 +0,0 @@
|
|||||||
# Repository Guidelines
|
|
||||||
|
|
||||||
## Project Structure & Module Organization
|
|
||||||
FastAPI backend code lives in `app/`: routers under `app/api/v1`, orchestration in `app/controllers`, schemas/models in `app/schemas` and `app/models`, and shared helpers in `app/utils`. Config defaults stay in `app/settings/config.py`, migrations in `migrations/`, and the service boots through `run.py`. Frontend assets reside in `web/` with source code in `web/src`, static files in `web/public`, and build toggles in `web/settings`; deployment collateral sits in `deploy/`.
|
|
||||||
|
|
||||||
## Build, Test, and Development Commands
|
|
||||||
- `make install` (uv) or `pip install -r requirements.txt` prepares backend deps; `pnpm i` handles `web/`.
|
|
||||||
- `make start` / `python run.py` launches the API against `db.sqlite3`; `cd web && pnpm dev` starts the SPA; `pnpm build` prepares production assets.
|
|
||||||
- `make check` runs Black+isort in check mode plus Ruff; `make format` applies fixes; `make lint` is Ruff-only.
|
|
||||||
- `make test` loads `.env` variables into the shell and executes `pytest -vv -s`; target files with `pytest tests/api/test_x.py -k keyword`.
|
|
||||||
- Database maintenance: `make migrate` (generate Aerich migrations), `make upgrade` (apply), `make clean-db` (reset SQLite + migrations).
|
|
||||||
|
|
||||||
## Coding Style & Naming Conventions
|
|
||||||
Python follows Black (120 columns), isort’s Black profile, and Ruff; keep modules snake_case and Pydantic models PascalCase. Vue code respects the repo ESLint + UnoCSS presets, uses TypeScript script blocks, and keeps component directories kebab-case; run `pnpm lint` or `pnpm lint:fix` as needed.
|
|
||||||
|
|
||||||
## Testing Guidelines
|
|
||||||
Back-end features need pytest coverage mirroring the `app` layout—e.g., `tests/api/v1/test_users.py` for router logic and async tests following the patterns in `test_dynamic_default.py`. Seed deterministic data via fixtures instead of the shared `db.sqlite3`, and document any `.env` flags a test requires. Frontend changes should gain vitest or Playwright checks under `web/tests` before UI regressions reach `main`.
|
|
||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
|
||||||
Stick to Conventional Commit prefixes already present (`feat:`, `refactor:`, `debug:`) and keep subject lines imperative with optional scopes (`feat(api):`). Each PR must summarize changes, list verification commands, reference related issues, and attach UI screenshots/GIFs when touching `web/`. Run `make check` and relevant tests locally, avoid committing `web/dist` or SQLite WAL files, and prefer small, reviewable diffs.
|
|
||||||
|
|
||||||
## Security & Configuration Tips
|
|
||||||
Secrets belong in `.env`, which `app/settings/config.py` loads automatically; rotate `SECRET_KEY`, JWT parameters, and database credentials before deployment. Swap the Tortoise connection from SQLite to MySQL/PostgreSQL by editing the provided templates and running `make migrate && make upgrade`. Lock down CORS (`CORS_ORIGINS`) before exposing the API publicly.
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
-- 新增交易管理菜单
|
|
||||||
-- 创建时间: 2025-11-13
|
|
||||||
|
|
||||||
-- 插入一级目录:交易管理
|
|
||||||
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
|
|
||||||
VALUES
|
|
||||||
(16, '交易管理', 'catalog', 'carbon:receipt', '/transaction', 3, 0, 0, 'Layout', 0, '/transaction/invoice', datetime('now'), datetime('now'));
|
|
||||||
|
|
||||||
-- 插入二级菜单:开票记录
|
|
||||||
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
|
|
||||||
VALUES
|
|
||||||
(17, '开票记录', 'menu', 'carbon:document', 'invoice', 1, 16, 0, '/transaction/invoice', 0, NULL, datetime('now'), datetime('now'));
|
|
||||||
|
|
||||||
-- 为管理员角色分配菜单权限
|
|
||||||
INSERT INTO role_menu (role_id, menu_id)
|
|
||||||
VALUES
|
|
||||||
(1, 16),
|
|
||||||
(1, 17);
|
|
||||||
|
|
||||||
-- 为普通用户角色分配菜单权限
|
|
||||||
INSERT INTO role_menu (role_id, menu_id)
|
|
||||||
VALUES
|
|
||||||
(2, 16),
|
|
||||||
(2, 17);
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
-- 新增估值管理菜单
|
|
||||||
-- 创建时间: 2025-11-13
|
|
||||||
|
|
||||||
-- 插入一级目录:估值管理
|
|
||||||
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
|
|
||||||
VALUES
|
|
||||||
(18, '估值管理', 'catalog', 'carbon:calculator', '/valuation', 4, 0, 0, 'Layout', 0, '/valuation/audit', datetime('now'), datetime('now'));
|
|
||||||
|
|
||||||
-- 插入二级菜单:审核列表
|
|
||||||
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
|
|
||||||
VALUES
|
|
||||||
(19, '审核列表', 'menu', 'carbon:task-approved', 'audit', 1, 18, 0, '/valuation/audit', 0, NULL, datetime('now'), datetime('now'));
|
|
||||||
|
|
||||||
-- 为管理员角色分配菜单权限
|
|
||||||
INSERT INTO role_menu (role_id, menu_id)
|
|
||||||
VALUES
|
|
||||||
(1, 18),
|
|
||||||
(1, 19);
|
|
||||||
|
|
||||||
-- 为普通用户角色分配菜单权限
|
|
||||||
INSERT INTO role_menu (role_id, menu_id)
|
|
||||||
VALUES
|
|
||||||
(2, 18),
|
|
||||||
(2, 19);
|
|
||||||
@ -49,30 +49,6 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"label_error": "错误页",
|
"label_error": "错误页",
|
||||||
"text_back_to_home": "返回首页"
|
"text_back_to_home": "返回首页"
|
||||||
},
|
|
||||||
"user_management": {
|
|
||||||
"label_user_management": "用户管理",
|
|
||||||
"label_user_list": "用户列表",
|
|
||||||
"label_phone": "手机号",
|
|
||||||
"label_wechat": "微信号",
|
|
||||||
"label_register_time": "注册时间",
|
|
||||||
"label_remark": "备注",
|
|
||||||
"label_data_count": "创建数据数量",
|
|
||||||
"label_status": "状态",
|
|
||||||
"label_actions": "操作",
|
|
||||||
"button_detail": "详情",
|
|
||||||
"button_freeze": "冻结",
|
|
||||||
"button_unfreeze": "解冻",
|
|
||||||
"button_add_user": "新建用户",
|
|
||||||
"placeholder_phone": "请输入手机号",
|
|
||||||
"placeholder_wechat": "请输入微信号",
|
|
||||||
"placeholder_remark": "请输入备注",
|
|
||||||
"message_phone_required": "请输入手机号",
|
|
||||||
"message_phone_format_error": "请输入正确的手机号格式",
|
|
||||||
"message_freeze_success": "已冻结该用户",
|
|
||||||
"message_unfreeze_success": "已解冻该用户",
|
|
||||||
"text_status_normal": "正常",
|
|
||||||
"text_status_frozen": "冻结"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
|||||||
@ -19,9 +19,9 @@
|
|||||||
"infoColorSuppl": "#4098FCFF",
|
"infoColorSuppl": "#4098FCFF",
|
||||||
|
|
||||||
"successColor": "#18A058FF",
|
"successColor": "#18A058FF",
|
||||||
"successColorHover": "#36AD6AFF",
|
"successColorHover": "#F4511E",
|
||||||
"successColorPressed": "#0C7A43FF",
|
"successColorPressed": "#0C7A43FF",
|
||||||
"successColorSuppl": "#36AD6AFF",
|
"successColorSuppl": "#F4511E",
|
||||||
|
|
||||||
"warningColor": "#F0A020FF",
|
"warningColor": "#F0A020FF",
|
||||||
"warningColorHover": "#FCB040FF",
|
"warningColorHover": "#FCB040FF",
|
||||||
|
|||||||
@ -1,281 +1,5 @@
|
|||||||
import { request } from '@/utils'
|
import { request } from '@/utils'
|
||||||
|
|
||||||
const baseValuationDetail = {
|
|
||||||
valuation_result: 1180000,
|
|
||||||
created_at: '2024-11-10T09:30:00Z',
|
|
||||||
reviewed_at: null,
|
|
||||||
status: 'pending',
|
|
||||||
admin_notes: null,
|
|
||||||
asset_name: '蜀绣传承精品',
|
|
||||||
institution: '天府非遗文化发展有限公司',
|
|
||||||
industry: '文化创意',
|
|
||||||
annual_revenue: 980000,
|
|
||||||
rd_investment: 165000,
|
|
||||||
three_year_income: [890000, 975000, 1180000],
|
|
||||||
funding_status: '国家资助',
|
|
||||||
inheritor_level: '市级传承人',
|
|
||||||
inheritor_age_count: [4, 6, 2],
|
|
||||||
inheritor_certificates: [
|
|
||||||
'https://dummyimage.com/120x80/edf2ff/409eff&text=证书A1',
|
|
||||||
'https://dummyimage.com/120x80/fef6f0/f0a020&text=证书A2',
|
|
||||||
],
|
|
||||||
heritage_level: '国家级非遗',
|
|
||||||
heritage_asset_level: '一级保护',
|
|
||||||
patent_application_no: '1111111,2222222',
|
|
||||||
historical_evidence: {
|
|
||||||
artifacts: 1,
|
|
||||||
ancient_literature: 2,
|
|
||||||
inheritor_testimony: 0,
|
|
||||||
modern_research: 3,
|
|
||||||
},
|
|
||||||
patent_certificates: ['https://dummyimage.com/120x80/e8f5e9/34a853&text=专利1'],
|
|
||||||
pattern_images: ['https://dummyimage.com/120x80/f3e8ff/9c27b0&text=纹样1'],
|
|
||||||
application_maturity: '推广阶段',
|
|
||||||
application_coverage: '全国覆盖',
|
|
||||||
cooperation_depth: '品牌联名',
|
|
||||||
offline_activities: 4,
|
|
||||||
platform_accounts: {
|
|
||||||
bilibili: { account: 'B站@蜀绣', likes: 1260, comments: 320, shares: 188 },
|
|
||||||
},
|
|
||||||
sales_volume: 5200,
|
|
||||||
link_views: 86500,
|
|
||||||
circulation: '500-1000份',
|
|
||||||
last_market_activity: '近3个月',
|
|
||||||
monthly_transaction: '50-100万元',
|
|
||||||
price_fluctuation: [1200, 3200],
|
|
||||||
model_value_b: 1250000,
|
|
||||||
market_value_c: 1180000,
|
|
||||||
final_value_ab: 1220000,
|
|
||||||
dynamic_pledge_rate: 0.62,
|
|
||||||
calculation_result: {
|
|
||||||
flow: [
|
|
||||||
{
|
|
||||||
title: '基础估值',
|
|
||||||
description: '基于近三年收益与研发投入的模型估值',
|
|
||||||
value: '¥1,250,000',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '市场对标',
|
|
||||||
description: '结合同类资产市场成交价修正',
|
|
||||||
value: '¥1,180,000',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '综合校准',
|
|
||||||
description: '叠加ESG、政策匹配度得出最终估值',
|
|
||||||
value: '¥1,220,000',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const valuationRecords = [
|
|
||||||
{
|
|
||||||
id: 2001,
|
|
||||||
phone: '13800138001',
|
|
||||||
wechat: 'zhangsan_wx',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2002,
|
|
||||||
phone: '13800138002',
|
|
||||||
wechat: 'lisi2024',
|
|
||||||
valuation_result: 880000,
|
|
||||||
created_at: '2024-11-09T14:20:00Z',
|
|
||||||
reviewed_at: '2024-11-09T16:45:00Z',
|
|
||||||
status: 'approved',
|
|
||||||
admin_notes: '评估结果合理,已通过审核',
|
|
||||||
asset_name: '景泰蓝掐丝珐琅',
|
|
||||||
institution: '京华非遗研究院',
|
|
||||||
application_maturity: '成熟期',
|
|
||||||
application_coverage: '华北地区',
|
|
||||||
cooperation_depth: '科技载体',
|
|
||||||
platform_accounts: {
|
|
||||||
douyin: { account: '抖音@景泰蓝工坊', likes: 2350, comments: 610, shares: 302 },
|
|
||||||
},
|
|
||||||
price_fluctuation: [980, 2680],
|
|
||||||
calculation_result: {
|
|
||||||
flow: [
|
|
||||||
{
|
|
||||||
title: '基础估值',
|
|
||||||
description: '模型估算品牌溢价后得出结果',
|
|
||||||
value: '¥900,000',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '市场对标',
|
|
||||||
description: '对比近六个月文博拍卖价格',
|
|
||||||
value: '¥860,000',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '综合校准',
|
|
||||||
description: '结合政策扶持与线上声量校准',
|
|
||||||
value: '¥880,000',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2003,
|
|
||||||
phone: '13800138003',
|
|
||||||
wechat: 'wangwu_user',
|
|
||||||
valuation_result: 2100000,
|
|
||||||
created_at: '2024-11-08T16:45:00Z',
|
|
||||||
reviewed_at: '2024-11-08T18:30:00Z',
|
|
||||||
status: 'approved',
|
|
||||||
admin_notes: '评估价格偏高,但审核通过',
|
|
||||||
asset_name: '苗绣银饰',
|
|
||||||
institution: '黔锦民族文化有限公司',
|
|
||||||
industry: '民族工艺',
|
|
||||||
funding_status: '地方配套资金',
|
|
||||||
inheritor_level: '国家级代表性传承人',
|
|
||||||
inheritor_age_count: [2, 3, 1],
|
|
||||||
application_coverage: '西南片区',
|
|
||||||
platform_accounts: {
|
|
||||||
kuaishou: { account: '快手@苗绣手作', likes: 1800, comments: 420, shares: 210 },
|
|
||||||
},
|
|
||||||
price_fluctuation: [2600, 5200],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2004,
|
|
||||||
phone: '13800138004',
|
|
||||||
wechat: 'zhaoliu_vip',
|
|
||||||
valuation_result: 560000,
|
|
||||||
created_at: '2024-11-07T11:15:00Z',
|
|
||||||
status: 'pending',
|
|
||||||
asset_name: '景德镇青花',
|
|
||||||
institution: '景尚文化传播有限公司',
|
|
||||||
industry: '陶瓷制造',
|
|
||||||
funding_status: '社会资本',
|
|
||||||
platform_accounts: {
|
|
||||||
bilibili: { account: 'B站@青花研习社', likes: 860, comments: 146, shares: 98 },
|
|
||||||
},
|
|
||||||
price_fluctuation: [560, 1200],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2005,
|
|
||||||
phone: '13800138005',
|
|
||||||
wechat: 'sunqi888',
|
|
||||||
valuation_result: 1680000,
|
|
||||||
created_at: '2024-11-06T08:30:00Z',
|
|
||||||
reviewed_at: '2024-11-06T10:15:00Z',
|
|
||||||
status: 'approved',
|
|
||||||
admin_notes: '评估数据完整,审核通过',
|
|
||||||
asset_name: '藏医药香丸',
|
|
||||||
institution: '高原本草研究中心',
|
|
||||||
industry: '中医药',
|
|
||||||
application_coverage: '西藏及周边',
|
|
||||||
cooperation_depth: '国家外交礼品',
|
|
||||||
platform_accounts: {
|
|
||||||
douyin: { account: '抖音@藏医手作', likes: 3120, comments: 815, shares: 356 },
|
|
||||||
},
|
|
||||||
price_fluctuation: [3200, 7600],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2006,
|
|
||||||
phone: '13800138006',
|
|
||||||
wechat: 'zhouba2024',
|
|
||||||
valuation_result: 950000,
|
|
||||||
created_at: '2024-11-05T13:20:00Z',
|
|
||||||
status: 'pending',
|
|
||||||
asset_name: '苏绣屏风',
|
|
||||||
institution: '苏澜绣坊',
|
|
||||||
funding_status: '企业自筹',
|
|
||||||
platform_accounts: {
|
|
||||||
bilibili: { account: 'B站@苏绣博物馆', likes: 980, comments: 240, shares: 130 },
|
|
||||||
},
|
|
||||||
price_fluctuation: [1500, 3600],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2007,
|
|
||||||
phone: '13800138007',
|
|
||||||
wechat: 'wujiu_user',
|
|
||||||
valuation_result: 3200000,
|
|
||||||
created_at: '2024-11-04T15:45:00Z',
|
|
||||||
reviewed_at: '2024-11-04T17:20:00Z',
|
|
||||||
status: 'approved',
|
|
||||||
admin_notes: '高价值资产,评估结果准确',
|
|
||||||
asset_name: '宋锦织造',
|
|
||||||
institution: '苏州织造研究所',
|
|
||||||
funding_status: '国家重点补贴',
|
|
||||||
inheritor_age_count: [6, 8, 4],
|
|
||||||
application_maturity: '成熟期',
|
|
||||||
cooperation_depth: '科技载体',
|
|
||||||
platform_accounts: {
|
|
||||||
douyin: { account: '抖音@宋锦织造', likes: 4800, comments: 1020, shares: 520 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2008,
|
|
||||||
phone: '13800138008',
|
|
||||||
wechat: 'zhengshi_vip',
|
|
||||||
valuation_result: 750000,
|
|
||||||
created_at: '2024-11-03T10:10:00Z',
|
|
||||||
reviewed_at: '2024-11-03T12:00:00Z',
|
|
||||||
status: 'approved',
|
|
||||||
admin_notes: '评估流程规范,结果可信',
|
|
||||||
asset_name: '黄梅挑花',
|
|
||||||
institution: '徽楚非遗中心',
|
|
||||||
cooperation_depth: '品牌联名',
|
|
||||||
price_fluctuation: [980, 1800],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2009,
|
|
||||||
phone: '13800138009',
|
|
||||||
wechat: 'chenjun2024',
|
|
||||||
valuation_result: 1890000,
|
|
||||||
created_at: '2024-11-02T14:30:00Z',
|
|
||||||
status: 'pending',
|
|
||||||
asset_name: '黎锦织造',
|
|
||||||
institution: '海南黎锦工坊',
|
|
||||||
funding_status: '国家资助',
|
|
||||||
application_coverage: '华南地区',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2010,
|
|
||||||
phone: '13800138010',
|
|
||||||
wechat: 'liuxia_user',
|
|
||||||
valuation_result: 430000,
|
|
||||||
created_at: '2024-11-01T11:45:00Z',
|
|
||||||
reviewed_at: '2024-11-01T13:30:00Z',
|
|
||||||
status: 'approved',
|
|
||||||
admin_notes: '低价值资产,评估合理',
|
|
||||||
asset_name: '大漆工艺',
|
|
||||||
institution: '榫卯器物社',
|
|
||||||
funding_status: '地方专项',
|
|
||||||
cooperation_depth: '品牌联名',
|
|
||||||
platform_accounts: {
|
|
||||||
bilibili: { account: 'B站@大漆工坊', likes: 420, comments: 75, shares: 33 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2011,
|
|
||||||
phone: '13800138011',
|
|
||||||
wechat: 'zhaolei2024',
|
|
||||||
valuation_result: 2100000,
|
|
||||||
created_at: '2024-10-31T09:20:00Z',
|
|
||||||
reviewed_at: '2024-10-31T11:00:00Z',
|
|
||||||
status: 'approved',
|
|
||||||
admin_notes: '评估报告详细,数据支撑充分',
|
|
||||||
asset_name: '龙泉青瓷',
|
|
||||||
institution: '浙瓷非遗研究院',
|
|
||||||
cooperation_depth: '科技载体',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2012,
|
|
||||||
phone: '13800138012',
|
|
||||||
wechat: 'sunmei_vip',
|
|
||||||
valuation_result: 680000,
|
|
||||||
created_at: '2024-10-30T16:15:00Z',
|
|
||||||
status: 'pending',
|
|
||||||
asset_name: '侗锦织造',
|
|
||||||
institution: '黔东南侗锦合作社',
|
|
||||||
funding_status: '社会资本',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const mockValuationDetails = valuationRecords.map((record) => ({
|
|
||||||
...baseValuationDetail,
|
|
||||||
...record,
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
login: (data) => request.post('/base/access_token', data, { noNeedToken: true }),
|
login: (data) => request.post('/base/access_token', data, { noNeedToken: true }),
|
||||||
getUserInfo: () => request.get('/base/userinfo'),
|
getUserInfo: () => request.get('/base/userinfo'),
|
||||||
@ -315,419 +39,4 @@ export default {
|
|||||||
deleteDept: (params = {}) => request.delete('/dept/delete', { params }),
|
deleteDept: (params = {}) => request.delete('/dept/delete', { params }),
|
||||||
// auditlog
|
// auditlog
|
||||||
getAuditLogList: (params = {}) => request.get('/auditlog/list', { params }),
|
getAuditLogList: (params = {}) => request.get('/auditlog/list', { params }),
|
||||||
// app users (客户端用户管理) - 使用现有的后端接口
|
|
||||||
getAppUserList: (params = {}) => {
|
|
||||||
// Mock 数据
|
|
||||||
const mockUsers = [
|
|
||||||
{
|
|
||||||
id: 11111111,
|
|
||||||
phone: '15021982682',
|
|
||||||
wechat: 'f1498480844',
|
|
||||||
created_at: '2024-01-15T10:30:00Z',
|
|
||||||
notes: '测试用户1',
|
|
||||||
remaining_count: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11111112,
|
|
||||||
phone: '13800138002',
|
|
||||||
wechat: 'wx_limming2024',
|
|
||||||
created_at: '2024-02-20T14:20:00Z',
|
|
||||||
notes: '付费用户',
|
|
||||||
remaining_count: 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11111113,
|
|
||||||
phone: '13800138003',
|
|
||||||
wechat: null,
|
|
||||||
created_at: '2024-03-10T08:45:00Z',
|
|
||||||
notes: null,
|
|
||||||
remaining_count: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11111114,
|
|
||||||
phone: '13800138004',
|
|
||||||
wechat: 'chenjun_vip',
|
|
||||||
created_at: '2024-04-05T11:30:00Z',
|
|
||||||
notes: 'VIP用户',
|
|
||||||
remaining_count: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11111115,
|
|
||||||
phone: '13800138005',
|
|
||||||
wechat: 'liuxia888',
|
|
||||||
created_at: '2024-05-12T16:15:00Z',
|
|
||||||
notes: '体验用户',
|
|
||||||
remaining_count: 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11111116,
|
|
||||||
phone: '13800138006',
|
|
||||||
wechat: null,
|
|
||||||
created_at: '2024-06-18T09:00:00Z',
|
|
||||||
notes: '新注册用户',
|
|
||||||
remaining_count: 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11111117,
|
|
||||||
phone: '13800138007',
|
|
||||||
wechat: 'zhaolei2024',
|
|
||||||
created_at: '2024-07-22T12:45:00Z',
|
|
||||||
notes: null,
|
|
||||||
remaining_count: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11111118,
|
|
||||||
phone: '13800138008',
|
|
||||||
wechat: 'sunmei_user',
|
|
||||||
created_at: '2024-08-30T15:20:00Z',
|
|
||||||
notes: '活跃用户',
|
|
||||||
remaining_count: 7
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 模拟分页和搜索
|
|
||||||
let filteredUsers = [...mockUsers]
|
|
||||||
|
|
||||||
// 手机号搜索
|
|
||||||
if (params.phone) {
|
|
||||||
filteredUsers = filteredUsers.filter(user =>
|
|
||||||
user.phone.includes(params.phone)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 微信号搜索
|
|
||||||
if (params.wechat) {
|
|
||||||
filteredUsers = filteredUsers.filter(user =>
|
|
||||||
user.wechat && user.wechat.includes(params.wechat)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页处理
|
|
||||||
const page = Number(params.page) || 1
|
|
||||||
const pageSize = Number(params.page_size) || 10
|
|
||||||
const startIndex = (page - 1) * pageSize
|
|
||||||
const endIndex = startIndex + pageSize
|
|
||||||
const paginatedUsers = filteredUsers.slice(startIndex, endIndex)
|
|
||||||
|
|
||||||
// 返回 Promise 模拟异步请求
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve({
|
|
||||||
data: paginatedUsers,
|
|
||||||
total: filteredUsers.length,
|
|
||||||
page: page,
|
|
||||||
page_size: pageSize
|
|
||||||
})
|
|
||||||
}, 300) // 模拟网络延迟
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getAppUserById: (params = {}) => request.get('/app-user/detail', { params }),
|
|
||||||
createAppUser: (data = {}) => request.post('/app-user/register', data),
|
|
||||||
updateAppUser: (data = {}) => request.post('/app-user/update', data),
|
|
||||||
deleteAppUser: (params = {}) => request.delete('/app-user/delete', { params }),
|
|
||||||
// invoice (开票记录)
|
|
||||||
getInvoiceList: (params = {}) => {
|
|
||||||
// Mock 数据
|
|
||||||
const mockInvoices = [
|
|
||||||
{
|
|
||||||
id: 1001,
|
|
||||||
created_at: '2024-11-10T09:30:00Z',
|
|
||||||
ticket_type: 'electronic',
|
|
||||||
phone: '13800138001',
|
|
||||||
email: 'zhangsan@company1.com',
|
|
||||||
company_name: '北京科技有限公司',
|
|
||||||
tax_number: '91110000123456789A',
|
|
||||||
register_address: '北京市朝阳区科技园区A座1001室',
|
|
||||||
register_phone: '010-12345678',
|
|
||||||
bank_name: '中国工商银行北京分行',
|
|
||||||
bank_account: '6222021234567890123',
|
|
||||||
invoice_type: 'special',
|
|
||||||
status: 'pending'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1002,
|
|
||||||
created_at: '2024-11-09T14:20:00Z',
|
|
||||||
ticket_type: 'paper',
|
|
||||||
phone: '13800138002',
|
|
||||||
email: 'lisi@company2.com',
|
|
||||||
company_name: '上海贸易股份有限公司',
|
|
||||||
tax_number: '91310000987654321B',
|
|
||||||
register_address: '上海市浦东新区金融街B座2002室',
|
|
||||||
register_phone: '021-87654321',
|
|
||||||
bank_name: '中国建设银行上海分行',
|
|
||||||
bank_account: '6217001234567890124',
|
|
||||||
invoice_type: 'normal',
|
|
||||||
status: 'invoiced'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1003,
|
|
||||||
created_at: '2024-11-08T16:45:00Z',
|
|
||||||
ticket_type: 'electronic',
|
|
||||||
phone: '13800138003',
|
|
||||||
email: 'wangwu@company3.com',
|
|
||||||
company_name: '深圳创新科技有限公司',
|
|
||||||
tax_number: '91440300456789012C',
|
|
||||||
register_address: '深圳市南山区高新技术园C座3003室',
|
|
||||||
register_phone: '0755-23456789',
|
|
||||||
bank_name: '招商银行深圳分行',
|
|
||||||
bank_account: '6214851234567890125',
|
|
||||||
invoice_type: 'special',
|
|
||||||
status: 'rejected'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1004,
|
|
||||||
created_at: '2024-11-07T11:15:00Z',
|
|
||||||
ticket_type: 'paper',
|
|
||||||
phone: '13800138004',
|
|
||||||
email: 'zhaoliu@company4.com',
|
|
||||||
company_name: '广州制造业集团有限公司',
|
|
||||||
tax_number: '91440100789012345D',
|
|
||||||
register_address: '广州市天河区商务中心D座4004室',
|
|
||||||
register_phone: '020-34567890',
|
|
||||||
bank_name: '中国银行广州分行',
|
|
||||||
bank_account: '6013821234567890126',
|
|
||||||
invoice_type: 'normal',
|
|
||||||
status: 'pending'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1005,
|
|
||||||
created_at: '2024-11-06T08:30:00Z',
|
|
||||||
ticket_type: 'electronic',
|
|
||||||
phone: '13800138005',
|
|
||||||
email: 'sunqi@company5.com',
|
|
||||||
company_name: '杭州互联网科技有限公司',
|
|
||||||
tax_number: '91330100012345678E',
|
|
||||||
register_address: '杭州市西湖区互联网小镇E座5005室',
|
|
||||||
register_phone: '0571-45678901',
|
|
||||||
bank_name: '浙商银行杭州分行',
|
|
||||||
bank_account: '6228481234567890127',
|
|
||||||
invoice_type: 'special',
|
|
||||||
status: 'invoiced'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1006,
|
|
||||||
created_at: '2024-11-05T13:20:00Z',
|
|
||||||
ticket_type: 'paper',
|
|
||||||
phone: '13800138006',
|
|
||||||
email: 'zhouba@company6.com',
|
|
||||||
company_name: '成都软件开发有限公司',
|
|
||||||
tax_number: '91510100345678901F',
|
|
||||||
register_address: '成都市高新区软件园F座6006室',
|
|
||||||
register_phone: '028-56789012',
|
|
||||||
bank_name: '中国农业银行成都分行',
|
|
||||||
bank_account: '6230521234567890128',
|
|
||||||
invoice_type: 'normal',
|
|
||||||
status: 'pending'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1007,
|
|
||||||
created_at: '2024-11-04T15:45:00Z',
|
|
||||||
ticket_type: 'electronic',
|
|
||||||
phone: '13800138007',
|
|
||||||
email: 'wujiu@company7.com',
|
|
||||||
company_name: '武汉新能源科技有限公司',
|
|
||||||
tax_number: '91420100678901234G',
|
|
||||||
register_address: '武汉市江汉区新能源产业园G座7007室',
|
|
||||||
register_phone: '027-67890123',
|
|
||||||
bank_name: '交通银行武汉分行',
|
|
||||||
bank_account: '6222601234567890129',
|
|
||||||
invoice_type: 'special',
|
|
||||||
status: 'invoiced'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1008,
|
|
||||||
created_at: '2024-11-03T10:10:00Z',
|
|
||||||
ticket_type: 'paper',
|
|
||||||
phone: '13800138008',
|
|
||||||
email: 'zhengshi@company8.com',
|
|
||||||
company_name: '西安电子商务有限公司',
|
|
||||||
tax_number: '91610100901234567H',
|
|
||||||
register_address: '西安市雁塔区电商产业园H座8008室',
|
|
||||||
register_phone: '029-78901234',
|
|
||||||
bank_name: '中信银行西安分行',
|
|
||||||
bank_account: '6217711234567890130',
|
|
||||||
invoice_type: 'normal',
|
|
||||||
status: 'refunded'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1009,
|
|
||||||
created_at: '2024-11-02T14:30:00Z',
|
|
||||||
ticket_type: 'electronic',
|
|
||||||
phone: '13800138009',
|
|
||||||
email: 'wangwu@company9.com',
|
|
||||||
company_name: '天津物流科技有限公司',
|
|
||||||
tax_number: '91120000234567890I',
|
|
||||||
register_address: '天津市滨海新区物流园I座9009室',
|
|
||||||
register_phone: '022-89012345',
|
|
||||||
bank_name: '民生银行天津分行',
|
|
||||||
bank_account: '6226221234567890131',
|
|
||||||
invoice_type: 'special',
|
|
||||||
status: 'refunded'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1010,
|
|
||||||
created_at: '2024-11-01T11:45:00Z',
|
|
||||||
ticket_type: 'paper',
|
|
||||||
phone: '13800138010',
|
|
||||||
email: 'liuliu@company10.com',
|
|
||||||
company_name: '重庆智能制造有限公司',
|
|
||||||
tax_number: '91500000567890123J',
|
|
||||||
register_address: '重庆市渝北区智能制造园J座1010室',
|
|
||||||
register_phone: '023-90123456',
|
|
||||||
bank_name: '华夏银行重庆分行',
|
|
||||||
bank_account: '6228881234567890132',
|
|
||||||
invoice_type: 'normal',
|
|
||||||
status: 'rejected'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 模拟分页和搜索
|
|
||||||
let filteredInvoices = [...mockInvoices]
|
|
||||||
|
|
||||||
// 手机号搜索
|
|
||||||
if (params.phone) {
|
|
||||||
filteredInvoices = filteredInvoices.filter(invoice =>
|
|
||||||
invoice.phone.includes(params.phone)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 公司名称搜索
|
|
||||||
if (params.company_name) {
|
|
||||||
filteredInvoices = filteredInvoices.filter(invoice =>
|
|
||||||
invoice.company_name.includes(params.company_name)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 公司税号搜索
|
|
||||||
if (params.tax_number) {
|
|
||||||
filteredInvoices = filteredInvoices.filter(invoice =>
|
|
||||||
invoice.tax_number.includes(params.tax_number)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态筛选
|
|
||||||
if (params.status) {
|
|
||||||
filteredInvoices = filteredInvoices.filter(invoice =>
|
|
||||||
invoice.status === params.status
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交时间筛选
|
|
||||||
if (params.created_at && Array.isArray(params.created_at) && params.created_at.length === 2) {
|
|
||||||
const [startDate, endDate] = params.created_at
|
|
||||||
filteredInvoices = filteredInvoices.filter(invoice => {
|
|
||||||
const invoiceDate = new Date(invoice.created_at)
|
|
||||||
return invoiceDate >= new Date(startDate) && invoiceDate <= new Date(endDate)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页处理
|
|
||||||
const page = params.page || 1
|
|
||||||
const pageSize = params.page_size || 10
|
|
||||||
const startIndex = (page - 1) * pageSize
|
|
||||||
const endIndex = startIndex + pageSize
|
|
||||||
const paginatedInvoices = filteredInvoices.slice(startIndex, endIndex)
|
|
||||||
|
|
||||||
// 返回 Promise 模拟异步请求
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve({
|
|
||||||
data: paginatedInvoices,
|
|
||||||
total: filteredInvoices.length,
|
|
||||||
page: page,
|
|
||||||
page_size: pageSize
|
|
||||||
})
|
|
||||||
}, 300) // 模拟网络延迟
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getInvoiceById: (params = {}) => request.get('/invoice/detail', { params }),
|
|
||||||
createInvoice: (data = {}) => request.post('/invoice/create', data),
|
|
||||||
updateInvoice: (data = {}) => request.post('/invoice/update', data),
|
|
||||||
deleteInvoice: (params = {}) => request.delete('/invoice/delete', { params }),
|
|
||||||
updateInvoiceStatus: (data = {}) => request.post('/invoice/update-status', data),
|
|
||||||
remindInvoice: (data = {}) => request.post('/invoice/remind', data),
|
|
||||||
refundInvoice: (data = {}) => request.post('/invoice/refund', data),
|
|
||||||
sendInvoice: (data = {}) => request.post('/invoice/send', data),
|
|
||||||
// valuation (估值评估)
|
|
||||||
getValuationList: (params = {}) => {
|
|
||||||
// 模拟分页和搜索
|
|
||||||
let filteredValuations = [...mockValuationDetails]
|
|
||||||
|
|
||||||
// 手机号搜索
|
|
||||||
if (params.phone) {
|
|
||||||
filteredValuations = filteredValuations.filter(valuation =>
|
|
||||||
valuation.phone.includes(params.phone)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 微信号搜索
|
|
||||||
if (params.wechat) {
|
|
||||||
filteredValuations = filteredValuations.filter(valuation =>
|
|
||||||
valuation.wechat && valuation.wechat.includes(params.wechat)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态筛选
|
|
||||||
if (params.status) {
|
|
||||||
filteredValuations = filteredValuations.filter(valuation =>
|
|
||||||
valuation.status === params.status
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交时间筛选
|
|
||||||
if (params.created_at && Array.isArray(params.created_at) && params.created_at.length === 2) {
|
|
||||||
const [startDate, endDate] = params.created_at
|
|
||||||
filteredValuations = filteredValuations.filter(valuation => {
|
|
||||||
const valuationDate = new Date(valuation.created_at)
|
|
||||||
return valuationDate >= new Date(startDate) && valuationDate <= new Date(endDate)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页处理
|
|
||||||
const page = params.page || 1
|
|
||||||
const pageSize = params.page_size || 10
|
|
||||||
const startIndex = (page - 1) * pageSize
|
|
||||||
const endIndex = startIndex + pageSize
|
|
||||||
const paginatedValuations = filteredValuations.slice(startIndex, endIndex).map((item) => ({
|
|
||||||
id: item.id,
|
|
||||||
phone: item.phone,
|
|
||||||
wechat: item.wechat,
|
|
||||||
valuation_result: item.valuation_result,
|
|
||||||
created_at: item.created_at,
|
|
||||||
reviewed_at: item.reviewed_at,
|
|
||||||
status: item.status,
|
|
||||||
admin_notes: item.admin_notes,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 返回 Promise 模拟异步请求
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve({
|
|
||||||
data: paginatedValuations,
|
|
||||||
total: filteredValuations.length,
|
|
||||||
page: page,
|
|
||||||
page_size: pageSize
|
|
||||||
})
|
|
||||||
}, 300) // 模拟网络延迟
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getValuationById: (params = {}) => {
|
|
||||||
const id = Number(params.valuation_id || params.id)
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const detail = mockValuationDetails.find((item) => item.id === id)
|
|
||||||
if (detail) {
|
|
||||||
resolve({ data: detail })
|
|
||||||
} else {
|
|
||||||
reject({ code: 404, message: '未找到估值详情' })
|
|
||||||
}
|
|
||||||
}, 200)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
createValuation: (data = {}) => request.post('/valuation', data),
|
|
||||||
updateValuation: (data = {}) => request.put(`/valuation/${data.id}`, data),
|
|
||||||
deleteValuation: (params = {}) => request.delete(`/valuation/${params.valuation_id}`),
|
|
||||||
approveValuation: (data = {}) => request.post(`/valuation/${data.valuation_id}/approve`, data),
|
|
||||||
rejectValuation: (data = {}) => request.post(`/valuation/${data.valuation_id}/reject`, data),
|
|
||||||
updateValuationNotes: (data = {}) => request.put(`/valuation/${data.valuation_id}/admin-notes`, data),
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,6 @@
|
|||||||
<slot name="queryBar" />
|
<slot name="queryBar" />
|
||||||
</QueryBar>
|
</QueryBar>
|
||||||
|
|
||||||
<!-- 操作按钮区域 -->
|
|
||||||
<div v-if="$slots.action" class="mb-16">
|
|
||||||
<slot name="action" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<n-data-table
|
<n-data-table
|
||||||
:remote="remote"
|
:remote="remote"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
|||||||
@ -59,10 +59,32 @@ function getMenuItem(route, basePath = '') {
|
|||||||
|
|
||||||
if (!visibleChildren.length) return menuItem
|
if (!visibleChildren.length) return menuItem
|
||||||
|
|
||||||
|
if (visibleChildren.length === 1) {
|
||||||
|
// 单个子路由处理
|
||||||
|
const singleRoute = visibleChildren[0]
|
||||||
|
menuItem = {
|
||||||
|
...menuItem,
|
||||||
|
label: singleRoute.meta?.title || singleRoute.name,
|
||||||
|
key: singleRoute.name,
|
||||||
|
path: resolvePath(menuItem.path, singleRoute.path),
|
||||||
|
icon: getIcon(singleRoute.meta),
|
||||||
|
}
|
||||||
|
const visibleItems = singleRoute.children
|
||||||
|
? singleRoute.children.filter((item) => item.name && !item.isHidden)
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (visibleItems.length === 1) {
|
||||||
|
menuItem = getMenuItem(visibleItems[0], menuItem.path)
|
||||||
|
} else if (visibleItems.length > 1) {
|
||||||
|
menuItem.children = visibleItems
|
||||||
|
.map((item) => getMenuItem(item, menuItem.path))
|
||||||
|
.sort((a, b) => a.order - b.order)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
menuItem.children = visibleChildren
|
menuItem.children = visibleChildren
|
||||||
.map((item) => getMenuItem(item, menuItem.path))
|
.map((item) => getMenuItem(item, menuItem.path))
|
||||||
.sort((a, b) => a.order - b.order)
|
.sort((a, b) => a.order - b.order)
|
||||||
|
}
|
||||||
return menuItem
|
return menuItem
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,11 +95,9 @@ function getIcon(meta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleMenuSelect(key, item) {
|
function handleMenuSelect(key, item) {
|
||||||
console.log("🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥 ~ handleMenuSelect ~ key, item:", key, item);
|
|
||||||
if (isExternal(item.path)) {
|
if (isExternal(item.path)) {
|
||||||
window.open(item.path)
|
window.open(item.path)
|
||||||
} else {
|
} else {
|
||||||
console.log("🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥 ~ handleMenuSelect ~ item.path === curRoute.path:", item.path === curRoute.path);
|
|
||||||
if (item.path === curRoute.path) {
|
if (item.path === curRoute.path) {
|
||||||
appStore.reloadPage()
|
appStore.reloadPage()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -6,17 +6,10 @@ import api from '@/api'
|
|||||||
// * 后端路由相关函数
|
// * 后端路由相关函数
|
||||||
// 根据后端传来数据构建出前端路由
|
// 根据后端传来数据构建出前端路由
|
||||||
|
|
||||||
function getRouteName(route) {
|
|
||||||
if (route?.id) return `menu-${route.id}`
|
|
||||||
if (route?.path) return route.path.replace(/\//g, '-') || route.name
|
|
||||||
return route?.name
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRoutes(routes = []) {
|
function buildRoutes(routes = []) {
|
||||||
return routes.map((e) => {
|
return routes.map((e) => {
|
||||||
const routeName = getRouteName(e)
|
|
||||||
const route = {
|
const route = {
|
||||||
name: routeName,
|
name: e.name,
|
||||||
path: e.path,
|
path: e.path,
|
||||||
component: shallowRef(Layout),
|
component: shallowRef(Layout),
|
||||||
isHidden: e.is_hidden,
|
isHidden: e.is_hidden,
|
||||||
@ -33,7 +26,7 @@ function buildRoutes(routes = []) {
|
|||||||
if (e.children && e.children.length > 0) {
|
if (e.children && e.children.length > 0) {
|
||||||
// 有子菜单
|
// 有子菜单
|
||||||
route.children = e.children.map((e_child) => ({
|
route.children = e.children.map((e_child) => ({
|
||||||
name: getRouteName(e_child),
|
name: e_child.name,
|
||||||
path: e_child.path,
|
path: e_child.path,
|
||||||
component: vueModules[`/src/views${e_child.component}/index.vue`],
|
component: vueModules[`/src/views${e_child.component}/index.vue`],
|
||||||
isHidden: e_child.is_hidden,
|
isHidden: e_child.is_hidden,
|
||||||
@ -47,7 +40,7 @@ function buildRoutes(routes = []) {
|
|||||||
} else {
|
} else {
|
||||||
// 没有子菜单,创建一个默认的子路由
|
// 没有子菜单,创建一个默认的子路由
|
||||||
route.children.push({
|
route.children.push({
|
||||||
name: `${routeName}Default`,
|
name: `${e.name}Default`,
|
||||||
path: '',
|
path: '',
|
||||||
component: vueModules[`/src/views${e.component}/index.vue`],
|
component: vueModules[`/src/views${e.component}/index.vue`],
|
||||||
isHidden: true,
|
isHidden: true,
|
||||||
@ -56,7 +49,6 @@ function buildRoutes(routes = []) {
|
|||||||
icon: e.icon,
|
icon: e.icon,
|
||||||
order: e.order,
|
order: e.order,
|
||||||
keepAlive: e.keepalive,
|
keepAlive: e.keepalive,
|
||||||
activeMenu: routeName,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,222 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import { NModal, NCard, NForm, NFormItem, NInput, NButton, NUpload, NSpace, NText } from 'naive-ui'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
invoiceData: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: 'invoice', // 'invoice' 或 'view'
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:visible', 'confirm'])
|
|
||||||
|
|
||||||
const formRef = ref(null)
|
|
||||||
const formData = ref({
|
|
||||||
email: '',
|
|
||||||
content: '',
|
|
||||||
attachments: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
const fileList = ref([])
|
|
||||||
|
|
||||||
// 监听弹窗打开,初始化数据
|
|
||||||
watch(
|
|
||||||
() => props.visible,
|
|
||||||
(val) => {
|
|
||||||
if (val) {
|
|
||||||
formData.value = {
|
|
||||||
email: props.invoiceData?.email || '',
|
|
||||||
content: '',
|
|
||||||
attachments: [],
|
|
||||||
}
|
|
||||||
fileList.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 关闭弹窗
|
|
||||||
const handleClose = () => {
|
|
||||||
emit('update:visible', false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认操作
|
|
||||||
const handleConfirm = () => {
|
|
||||||
formRef.value?.validate((errors) => {
|
|
||||||
if (!errors) {
|
|
||||||
emit('confirm', {
|
|
||||||
...formData.value,
|
|
||||||
id: props.invoiceData?.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文件上传前的处理
|
|
||||||
const beforeUpload = (data) => {
|
|
||||||
const { file } = data
|
|
||||||
const isImage = file.type.startsWith('image/')
|
|
||||||
const isPdf = file.type === 'application/pdf'
|
|
||||||
|
|
||||||
if (!isImage && !isPdf) {
|
|
||||||
$message.error('只能上传图片或PDF文件')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const isLt10M = file.size / 1024 / 1024 < 10
|
|
||||||
if (!isLt10M) {
|
|
||||||
$message.error('文件大小不能超过10MB')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文件上传变化
|
|
||||||
const handleUploadChange = ({ fileList: newFileList }) => {
|
|
||||||
fileList.value = newFileList
|
|
||||||
formData.value.attachments = newFileList.map(file => file.id || file.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除文件
|
|
||||||
const handleRemove = ({ file }) => {
|
|
||||||
const index = fileList.value.findIndex(item => item.id === file.id)
|
|
||||||
if (index > -1) {
|
|
||||||
fileList.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
email: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请输入发送邮箱',
|
|
||||||
trigger: ['input', 'blur'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'email',
|
|
||||||
message: '请输入正确的邮箱格式',
|
|
||||||
trigger: ['input', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const modalTitle = props.mode === 'invoice' ? '开票' : '查看发票'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<NModal
|
|
||||||
:show="visible"
|
|
||||||
:mask-closable="false"
|
|
||||||
preset="card"
|
|
||||||
:title="modalTitle"
|
|
||||||
class="invoice-modal"
|
|
||||||
style="width: 600px"
|
|
||||||
@update:show="handleClose"
|
|
||||||
>
|
|
||||||
<NForm
|
|
||||||
ref="formRef"
|
|
||||||
:model="formData"
|
|
||||||
:rules="rules"
|
|
||||||
label-placement="left"
|
|
||||||
label-width="100"
|
|
||||||
>
|
|
||||||
<NFormItem label="发送邮箱" path="email">
|
|
||||||
<NInput
|
|
||||||
v-model:value="formData.email"
|
|
||||||
placeholder="请输入邮箱地址"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</NFormItem>
|
|
||||||
|
|
||||||
<NFormItem label="文案">
|
|
||||||
<NInput
|
|
||||||
v-model:value="formData.content"
|
|
||||||
type="textarea"
|
|
||||||
placeholder="请输入文案内容"
|
|
||||||
:rows="4"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</NFormItem>
|
|
||||||
|
|
||||||
<NFormItem label="发票附件">
|
|
||||||
<div style="width: 100%">
|
|
||||||
<NUpload
|
|
||||||
v-model:file-list="fileList"
|
|
||||||
multiple
|
|
||||||
:max="2"
|
|
||||||
list-type="image-card"
|
|
||||||
:before-upload="beforeUpload"
|
|
||||||
@change="handleUploadChange"
|
|
||||||
@remove="handleRemove"
|
|
||||||
>
|
|
||||||
<div class="upload-trigger">
|
|
||||||
<div class="upload-icon">+</div>
|
|
||||||
</div>
|
|
||||||
</NUpload>
|
|
||||||
</div>
|
|
||||||
</NFormItem>
|
|
||||||
|
|
||||||
<NFormItem>
|
|
||||||
<div style="width: 100%; text-align: left; padding-left: 100px">
|
|
||||||
<NText depth="3" style="font-size: 12px; color: #ff9800">
|
|
||||||
ps:最多上传两张
|
|
||||||
</NText>
|
|
||||||
</div>
|
|
||||||
</NFormItem>
|
|
||||||
</NForm>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div style="display: flex; justify-content: flex-end; gap: 12px">
|
|
||||||
<NButton @click="handleClose">取消</NButton>
|
|
||||||
<NButton type="primary" @click="handleConfirm">确定</NButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</NModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.invoice-modal {
|
|
||||||
max-width: 90vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-trigger {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
border: 2px dashed #d9d9d9;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-trigger:hover {
|
|
||||||
border-color: #40a9ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-icon {
|
|
||||||
font-size: 32px;
|
|
||||||
color: #d9d9d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-upload-file-list) {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-upload-trigger) {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,503 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { h, onMounted, ref, resolveDirective, withDirectives } from 'vue'
|
|
||||||
import {
|
|
||||||
NButton,
|
|
||||||
NForm,
|
|
||||||
NFormItem,
|
|
||||||
NInput,
|
|
||||||
NTag,
|
|
||||||
NPopconfirm,
|
|
||||||
NSelect,
|
|
||||||
NDatePicker,
|
|
||||||
} from 'naive-ui'
|
|
||||||
|
|
||||||
import CommonPage from '@/components/page/CommonPage.vue'
|
|
||||||
import QueryBarItem from '@/components/query-bar/QueryBarItem.vue'
|
|
||||||
import CrudModal from '@/components/table/CrudModal.vue'
|
|
||||||
import CrudTable from '@/components/table/CrudTable.vue'
|
|
||||||
import InvoiceModal from './InvoiceModal.vue'
|
|
||||||
|
|
||||||
import { formatDate, renderIcon } from '@/utils'
|
|
||||||
import { useCRUD } from '@/composables'
|
|
||||||
import api from '@/api'
|
|
||||||
import TheIcon from '@/components/icon/TheIcon.vue'
|
|
||||||
|
|
||||||
defineOptions({ name: '开票记录' })
|
|
||||||
|
|
||||||
const $table = ref(null)
|
|
||||||
const queryItems = ref({})
|
|
||||||
const vPermission = resolveDirective('permission')
|
|
||||||
|
|
||||||
// 开票/查看弹窗相关状态
|
|
||||||
const invoiceModalVisible = ref(false)
|
|
||||||
const invoiceModalMode = ref('invoice') // 'invoice' 或 'view'
|
|
||||||
const currentInvoice = ref(null)
|
|
||||||
|
|
||||||
// 状态选项
|
|
||||||
const statusOptions = [
|
|
||||||
{ label: '全部', value: '' },
|
|
||||||
{ label: '未开票', value: 'pending' },
|
|
||||||
{ label: '已开票', value: 'invoiced' },
|
|
||||||
{ label: '已退款', value: 'refunded' },
|
|
||||||
{ label: '已拒绝', value: 'rejected' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 开票类型选项
|
|
||||||
const invoiceTypeOptions = [
|
|
||||||
{ label: '增值税普通发票', value: 'normal' },
|
|
||||||
{ label: '增值税专用发票', value: 'special' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 供票类型选项
|
|
||||||
const ticketTypeOptions = [
|
|
||||||
{ label: '纸质发票', value: 'paper' },
|
|
||||||
{ label: '电子发票', value: 'electronic' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const {
|
|
||||||
modalVisible,
|
|
||||||
modalTitle,
|
|
||||||
modalAction,
|
|
||||||
modalLoading,
|
|
||||||
handleSave,
|
|
||||||
modalForm,
|
|
||||||
modalFormRef,
|
|
||||||
handleEdit,
|
|
||||||
handleDelete,
|
|
||||||
handleAdd,
|
|
||||||
} = useCRUD({
|
|
||||||
name: '开票记录',
|
|
||||||
initForm: {},
|
|
||||||
doCreate: api.createInvoice,
|
|
||||||
doUpdate: api.updateInvoice,
|
|
||||||
doDelete: api.deleteInvoice,
|
|
||||||
refresh: () => $table.value?.handleSearch(),
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
$table.value?.handleSearch()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 状态标签渲染
|
|
||||||
const renderStatus = (status) => {
|
|
||||||
const statusMap = {
|
|
||||||
pending: { type: 'warning', text: '未开票' },
|
|
||||||
invoiced: { type: 'success', text: '已开票' },
|
|
||||||
refunded: { type: 'info', text: '已退款' },
|
|
||||||
rejected: { type: 'error', text: '已拒绝' },
|
|
||||||
}
|
|
||||||
const config = statusMap[status] || { type: 'default', text: '未知' }
|
|
||||||
return h(NTag, { type: config.type }, { default: () => config.text })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开票类型渲染
|
|
||||||
const renderInvoiceType = (type) => {
|
|
||||||
const typeMap = {
|
|
||||||
normal: '增值税普通发票',
|
|
||||||
special: '增值税专用发票',
|
|
||||||
}
|
|
||||||
return typeMap[type] || type
|
|
||||||
}
|
|
||||||
|
|
||||||
// 供票类型渲染
|
|
||||||
const renderTicketType = (type) => {
|
|
||||||
const typeMap = {
|
|
||||||
paper: '纸质发票',
|
|
||||||
electronic: '电子发票',
|
|
||||||
}
|
|
||||||
return typeMap[type] || type
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
key: 'id',
|
|
||||||
width: 60,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '提交时间',
|
|
||||||
key: 'created_at',
|
|
||||||
width: 100,
|
|
||||||
align: 'center',
|
|
||||||
render(row) {
|
|
||||||
return formatDate(row.created_at)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '供票类型',
|
|
||||||
key: 'ticket_type',
|
|
||||||
width: 80,
|
|
||||||
align: 'center',
|
|
||||||
render(row) {
|
|
||||||
return renderTicketType(row.ticket_type)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '手机号',
|
|
||||||
key: 'phone',
|
|
||||||
width: 100,
|
|
||||||
align: 'center',
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '邮箱号',
|
|
||||||
key: 'email',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '公司名称',
|
|
||||||
key: 'company_name',
|
|
||||||
width: 150,
|
|
||||||
align: 'center',
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '公司税号',
|
|
||||||
key: 'tax_number',
|
|
||||||
width: 150,
|
|
||||||
align: 'center',
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '注册地址',
|
|
||||||
key: 'register_address',
|
|
||||||
width: 150,
|
|
||||||
align: 'center',
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '注册电话',
|
|
||||||
key: 'register_phone',
|
|
||||||
width: 100,
|
|
||||||
align: 'center',
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '开户银行',
|
|
||||||
key: 'bank_name',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '银行账号',
|
|
||||||
key: 'bank_account',
|
|
||||||
width: 150,
|
|
||||||
align: 'center',
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '开票类型',
|
|
||||||
key: 'invoice_type',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
render(row) {
|
|
||||||
return renderInvoiceType(row.invoice_type)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
key: 'status',
|
|
||||||
width: 80,
|
|
||||||
align: 'center',
|
|
||||||
render(row) {
|
|
||||||
return renderStatus(row.status)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'actions',
|
|
||||||
width: 180,
|
|
||||||
align: 'center',
|
|
||||||
fixed: 'right',
|
|
||||||
render(row) {
|
|
||||||
return [
|
|
||||||
// 开票按钮 - 未开票状态显示
|
|
||||||
row.status === 'pending' &&
|
|
||||||
h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
size: 'small',
|
|
||||||
type: 'success',
|
|
||||||
style: 'margin-right: 8px;',
|
|
||||||
onClick: () => handleInvoice(row),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () => '开票',
|
|
||||||
}
|
|
||||||
),
|
|
||||||
// 退款按钮 - 未开票状态显示
|
|
||||||
row.status === 'pending' &&
|
|
||||||
h(
|
|
||||||
NPopconfirm,
|
|
||||||
{
|
|
||||||
onPositiveClick: () => handleRefund(row),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
trigger: () =>
|
|
||||||
h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
size: 'small',
|
|
||||||
type: 'primary',
|
|
||||||
style: 'margin-right: 8px;',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () => '退款',
|
|
||||||
}
|
|
||||||
),
|
|
||||||
default: () => h('div', {}, '确认退款?'),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
// 查看按钮 - 已开票状态显示
|
|
||||||
row.status === 'invoiced' &&
|
|
||||||
h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
size: 'small',
|
|
||||||
type: 'warning',
|
|
||||||
style: 'margin-right: 8px;',
|
|
||||||
onClick: () => handleView(row),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () => '查看',
|
|
||||||
}
|
|
||||||
),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// 更新状态
|
|
||||||
async function handleUpdateStatus(row, status) {
|
|
||||||
try {
|
|
||||||
await api.updateInvoiceStatus({ id: row.id, status })
|
|
||||||
$message.success('开票成功')
|
|
||||||
$table.value?.handleSearch()
|
|
||||||
} catch (error) {
|
|
||||||
$message.error('开票失败: ' + error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 退款处理
|
|
||||||
async function handleRefund(row) {
|
|
||||||
try {
|
|
||||||
// 如果是pending状态,直接退款并更新状态为refunded
|
|
||||||
if (row.status === 'pending') {
|
|
||||||
await api.updateInvoiceStatus({ id: row.id, status: 'refunded' })
|
|
||||||
$message.success('退款成功')
|
|
||||||
} else {
|
|
||||||
await api.refundInvoice({ id: row.id })
|
|
||||||
$message.success('退款成功')
|
|
||||||
}
|
|
||||||
$table.value?.handleSearch()
|
|
||||||
} catch (error) {
|
|
||||||
$message.error('退款失败: ' + error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开票处理
|
|
||||||
function handleInvoice(row) {
|
|
||||||
currentInvoice.value = row
|
|
||||||
invoiceModalMode.value = 'invoice'
|
|
||||||
invoiceModalVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查看详情
|
|
||||||
function handleView(row) {
|
|
||||||
currentInvoice.value = row
|
|
||||||
invoiceModalMode.value = 'view'
|
|
||||||
invoiceModalVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认开票/查看操作
|
|
||||||
async function handleInvoiceConfirm(data) {
|
|
||||||
try {
|
|
||||||
if (invoiceModalMode.value === 'invoice') {
|
|
||||||
// 开票操作
|
|
||||||
await api.sendInvoice(data)
|
|
||||||
await api.updateInvoiceStatus({ id: data.id, status: 'invoiced' })
|
|
||||||
$message.success('开票成功')
|
|
||||||
} else {
|
|
||||||
// 查看操作(可能是重新发送)
|
|
||||||
await api.sendInvoice(data)
|
|
||||||
$message.success('发送成功')
|
|
||||||
}
|
|
||||||
invoiceModalVisible.value = false
|
|
||||||
$table.value?.handleSearch()
|
|
||||||
} catch (error) {
|
|
||||||
$message.error('操作失败: ' + error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateForm = {
|
|
||||||
company_name: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请输入公司名称',
|
|
||||||
trigger: ['input', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
tax_number: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请输入公司税号',
|
|
||||||
trigger: ['input', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
phone: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请输入手机号',
|
|
||||||
trigger: ['input', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
email: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请输入邮箱',
|
|
||||||
trigger: ['input', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CommonPage show-footer title="开票记录">
|
|
||||||
<!-- 表格 -->
|
|
||||||
<CrudTable
|
|
||||||
ref="$table"
|
|
||||||
v-model:query-items="queryItems"
|
|
||||||
:columns="columns"
|
|
||||||
:get-data="api.getInvoiceList"
|
|
||||||
>
|
|
||||||
<template #queryBar>
|
|
||||||
<QueryBarItem label="提交时间" :label-width="80">
|
|
||||||
<NDatePicker
|
|
||||||
v-model:value="queryItems.created_at"
|
|
||||||
type="daterange"
|
|
||||||
clearable
|
|
||||||
placeholder="请选择提交时间"
|
|
||||||
style="width: 280px"
|
|
||||||
@update:value="$table?.handleSearch()"
|
|
||||||
/>
|
|
||||||
</QueryBarItem>
|
|
||||||
<QueryBarItem label="手机号" :label-width="80">
|
|
||||||
<NInput
|
|
||||||
v-model:value="queryItems.phone"
|
|
||||||
clearable
|
|
||||||
type="text"
|
|
||||||
placeholder="请输入手机号"
|
|
||||||
style="width: 200px"
|
|
||||||
@keypress.enter="$table?.handleSearch()"
|
|
||||||
/>
|
|
||||||
</QueryBarItem>
|
|
||||||
<QueryBarItem label="状态" :label-width="80">
|
|
||||||
<NSelect
|
|
||||||
v-model:value="queryItems.status"
|
|
||||||
:options="statusOptions"
|
|
||||||
placeholder="请选择状态"
|
|
||||||
clearable
|
|
||||||
style="width: 200px"
|
|
||||||
@update:value="$table?.handleSearch()"
|
|
||||||
/>
|
|
||||||
</QueryBarItem>
|
|
||||||
<QueryBarItem label="公司名称" :label-width="80">
|
|
||||||
<NInput
|
|
||||||
v-model:value="queryItems.company_name"
|
|
||||||
clearable
|
|
||||||
type="text"
|
|
||||||
placeholder="请输入公司名称"
|
|
||||||
style="width: 200px"
|
|
||||||
@keypress.enter="$table?.handleSearch()"
|
|
||||||
/>
|
|
||||||
</QueryBarItem>
|
|
||||||
<QueryBarItem label="公司税号" :label-width="80">
|
|
||||||
<NInput
|
|
||||||
v-model:value="queryItems.tax_number"
|
|
||||||
clearable
|
|
||||||
type="text"
|
|
||||||
placeholder="请输入公司税号"
|
|
||||||
style="width: 200px"
|
|
||||||
@keypress.enter="$table?.handleSearch()"
|
|
||||||
/>
|
|
||||||
</QueryBarItem>
|
|
||||||
</template>
|
|
||||||
</CrudTable>
|
|
||||||
|
|
||||||
<!-- 新增/编辑 弹窗 -->
|
|
||||||
<CrudModal
|
|
||||||
v-model:visible="modalVisible"
|
|
||||||
:title="modalTitle"
|
|
||||||
:loading="modalLoading"
|
|
||||||
@save="handleSave"
|
|
||||||
>
|
|
||||||
<NForm
|
|
||||||
ref="modalFormRef"
|
|
||||||
label-placement="left"
|
|
||||||
label-align="left"
|
|
||||||
:label-width="100"
|
|
||||||
:model="modalForm"
|
|
||||||
:rules="validateForm"
|
|
||||||
>
|
|
||||||
<NFormItem label="公司名称" path="company_name">
|
|
||||||
<NInput v-model:value="modalForm.company_name" clearable placeholder="请输入公司名称" />
|
|
||||||
</NFormItem>
|
|
||||||
<NFormItem label="公司税号" path="tax_number">
|
|
||||||
<NInput v-model:value="modalForm.tax_number" clearable placeholder="请输入公司税号" />
|
|
||||||
</NFormItem>
|
|
||||||
<NFormItem label="手机号" path="phone">
|
|
||||||
<NInput v-model:value="modalForm.phone" clearable placeholder="请输入手机号" />
|
|
||||||
</NFormItem>
|
|
||||||
<NFormItem label="邮箱" path="email">
|
|
||||||
<NInput v-model:value="modalForm.email" clearable placeholder="请输入邮箱" />
|
|
||||||
</NFormItem>
|
|
||||||
<NFormItem label="注册地址" path="register_address">
|
|
||||||
<NInput
|
|
||||||
v-model:value="modalForm.register_address"
|
|
||||||
clearable
|
|
||||||
placeholder="请输入注册地址"
|
|
||||||
/>
|
|
||||||
</NFormItem>
|
|
||||||
<NFormItem label="注册电话" path="register_phone">
|
|
||||||
<NInput
|
|
||||||
v-model:value="modalForm.register_phone"
|
|
||||||
clearable
|
|
||||||
placeholder="请输入注册电话"
|
|
||||||
/>
|
|
||||||
</NFormItem>
|
|
||||||
<NFormItem label="开户银行" path="bank_name">
|
|
||||||
<NInput v-model:value="modalForm.bank_name" clearable placeholder="请输入开户银行" />
|
|
||||||
</NFormItem>
|
|
||||||
<NFormItem label="银行账号" path="bank_account">
|
|
||||||
<NInput v-model:value="modalForm.bank_account" clearable placeholder="请输入银行账号" />
|
|
||||||
</NFormItem>
|
|
||||||
<NFormItem label="供票类型" path="ticket_type">
|
|
||||||
<NSelect
|
|
||||||
v-model:value="modalForm.ticket_type"
|
|
||||||
:options="ticketTypeOptions"
|
|
||||||
placeholder="请选择供票类型"
|
|
||||||
/>
|
|
||||||
</NFormItem>
|
|
||||||
<NFormItem label="开票类型" path="invoice_type">
|
|
||||||
<NSelect
|
|
||||||
v-model:value="modalForm.invoice_type"
|
|
||||||
:options="invoiceTypeOptions"
|
|
||||||
placeholder="请选择开票类型"
|
|
||||||
/>
|
|
||||||
</NFormItem>
|
|
||||||
</NForm>
|
|
||||||
</CrudModal>
|
|
||||||
|
|
||||||
<!-- 开票/查看弹窗 -->
|
|
||||||
<InvoiceModal
|
|
||||||
v-model:visible="invoiceModalVisible"
|
|
||||||
:invoice-data="currentInvoice"
|
|
||||||
:mode="invoiceModalMode"
|
|
||||||
@confirm="handleInvoiceConfirm"
|
|
||||||
/>
|
|
||||||
</CommonPage>
|
|
||||||
</template>
|
|
||||||
@ -1,208 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import {
|
|
||||||
NModal,
|
|
||||||
NButton,
|
|
||||||
NSelect,
|
|
||||||
NInput,
|
|
||||||
NDivider,
|
|
||||||
} from 'naive-ui'
|
|
||||||
|
|
||||||
// Props
|
|
||||||
const props = defineProps({
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
userData: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Emits
|
|
||||||
const emit = defineEmits(['update:visible', 'save'])
|
|
||||||
|
|
||||||
// 本地状态
|
|
||||||
const limitForm = ref({
|
|
||||||
remainingCount: 0,
|
|
||||||
type: '免费体验',
|
|
||||||
experienceCount: 1,
|
|
||||||
notes: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// 类型选项
|
|
||||||
const typeOptions = [
|
|
||||||
{ label: '免费体验', value: '免费体验' },
|
|
||||||
{ label: '付费用户', value: '付费用户' },
|
|
||||||
{ label: 'VIP用户', value: 'VIP用户' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 监听用户数据变化,初始化表单
|
|
||||||
watch(() => props.userData, (newData) => {
|
|
||||||
if (newData && Object.keys(newData).length > 0) {
|
|
||||||
limitForm.value = {
|
|
||||||
remainingCount: newData.remaining_count || 0,
|
|
||||||
type: newData.user_type || '免费体验',
|
|
||||||
experienceCount: newData.experience_count || 1,
|
|
||||||
notes: newData.notes || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
// 体验次数增减
|
|
||||||
function handleExperienceCountChange(delta) {
|
|
||||||
const newCount = limitForm.value.experienceCount + delta
|
|
||||||
if (newCount >= 0) {
|
|
||||||
limitForm.value.experienceCount = newCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存设置
|
|
||||||
function handleSave() {
|
|
||||||
const data = {
|
|
||||||
user_id: props.userData.id,
|
|
||||||
...limitForm.value
|
|
||||||
}
|
|
||||||
emit('save', data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消操作
|
|
||||||
function handleCancel() {
|
|
||||||
emit('update:visible', false)
|
|
||||||
// 重置表单
|
|
||||||
limitForm.value = {
|
|
||||||
remainingCount: 0,
|
|
||||||
type: '免费体验',
|
|
||||||
experienceCount: 1,
|
|
||||||
notes: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<NModal
|
|
||||||
:show="visible"
|
|
||||||
preset="card"
|
|
||||||
title="估值设置"
|
|
||||||
style="width: 500px;"
|
|
||||||
:bordered="false"
|
|
||||||
size="huge"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
@update:show="$emit('update:visible', $event)"
|
|
||||||
>
|
|
||||||
<div class="limit-setting-form">
|
|
||||||
<!-- 剩余估值次数 -->
|
|
||||||
<div class="form-row">
|
|
||||||
<span class="label">剩余估值次数:</span>
|
|
||||||
<span class="value">{{ limitForm.remainingCount }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<NDivider style="margin: 16px 0;" />
|
|
||||||
|
|
||||||
<!-- 类型选择 -->
|
|
||||||
<div class="form-row">
|
|
||||||
<span class="label">类型:</span>
|
|
||||||
<NSelect
|
|
||||||
v-model:value="limitForm.type"
|
|
||||||
:options="typeOptions"
|
|
||||||
style="width: 120px;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 体验次数 -->
|
|
||||||
<div class="form-row">
|
|
||||||
<span class="label">体验次数:</span>
|
|
||||||
<div class="count-control">
|
|
||||||
<NButton
|
|
||||||
size="small"
|
|
||||||
@click="handleExperienceCountChange(-1)"
|
|
||||||
:disabled="limitForm.experienceCount <= 0"
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</NButton>
|
|
||||||
<span class="count-value">{{ limitForm.experienceCount }}</span>
|
|
||||||
<NButton
|
|
||||||
size="small"
|
|
||||||
@click="handleExperienceCountChange(1)"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</NButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 备注 -->
|
|
||||||
<div class="form-row notes-row">
|
|
||||||
<span class="label">备注:</span>
|
|
||||||
</div>
|
|
||||||
<NInput
|
|
||||||
v-model:value="limitForm.notes"
|
|
||||||
type="textarea"
|
|
||||||
placeholder="请输入备注信息"
|
|
||||||
:rows="4"
|
|
||||||
style="margin-top: 8px;"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
|
||||||
<div class="action-buttons">
|
|
||||||
<NButton @click="handleCancel">
|
|
||||||
取消
|
|
||||||
</NButton>
|
|
||||||
<NButton type="primary" @click="handleSave">
|
|
||||||
确定
|
|
||||||
</NButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</NModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.limit-setting-form {
|
|
||||||
padding: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notes-row {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
min-width: 100px;
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.count-control {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.count-value {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
min-width: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 24px;
|
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,276 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { h, onMounted, ref, resolveDirective, withDirectives } from 'vue'
|
|
||||||
import {
|
|
||||||
NButton,
|
|
||||||
NForm,
|
|
||||||
NFormItem,
|
|
||||||
NInput,
|
|
||||||
NSpace,
|
|
||||||
NSwitch,
|
|
||||||
NTag,
|
|
||||||
NPopconfirm,
|
|
||||||
} from 'naive-ui'
|
|
||||||
|
|
||||||
import CommonPage from '@/components/page/CommonPage.vue'
|
|
||||||
import QueryBarItem from '@/components/query-bar/QueryBarItem.vue'
|
|
||||||
import CrudModal from '@/components/table/CrudModal.vue'
|
|
||||||
import CrudTable from '@/components/table/CrudTable.vue'
|
|
||||||
import LimitSettingModal from './LimitSettingModal.vue'
|
|
||||||
|
|
||||||
import { formatDate, renderIcon } from '@/utils'
|
|
||||||
import { useCRUD } from '@/composables'
|
|
||||||
import api from '@/api'
|
|
||||||
import TheIcon from '@/components/icon/TheIcon.vue'
|
|
||||||
|
|
||||||
defineOptions({ name: '用户管理' })
|
|
||||||
|
|
||||||
const $table = ref(null)
|
|
||||||
const queryItems = ref({})
|
|
||||||
const vPermission = resolveDirective('permission')
|
|
||||||
|
|
||||||
// 次数设置弹窗相关状态
|
|
||||||
const limitModalVisible = ref(false)
|
|
||||||
const currentUser = ref(null)
|
|
||||||
|
|
||||||
const {
|
|
||||||
modalVisible,
|
|
||||||
modalTitle,
|
|
||||||
modalAction,
|
|
||||||
modalLoading,
|
|
||||||
handleSave,
|
|
||||||
modalForm,
|
|
||||||
modalFormRef,
|
|
||||||
handleEdit,
|
|
||||||
handleDelete,
|
|
||||||
handleAdd,
|
|
||||||
} = useCRUD({
|
|
||||||
name: '用户',
|
|
||||||
initForm: { is_active: true },
|
|
||||||
doCreate: (data) => api.createAppUser({ phone: data.phone }),
|
|
||||||
doUpdate: (data) => api.updateAppUser(data),
|
|
||||||
doDelete: (data) => api.deleteAppUser({ id: data.id }),
|
|
||||||
refresh: () => $table.value?.handleSearch(),
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
$table.value?.handleSearch()
|
|
||||||
})
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
key: 'id',
|
|
||||||
width: 80,
|
|
||||||
align: 'center',
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '手机号',
|
|
||||||
key: 'phone',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '微信号',
|
|
||||||
key: 'wechat',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
render(row) {
|
|
||||||
return row.wechat || '-'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '注册时间',
|
|
||||||
key: 'created_at',
|
|
||||||
align: 'center',
|
|
||||||
width: 160,
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
render(row) {
|
|
||||||
return row.created_at ? formatDate(row.created_at) : '-'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '备注',
|
|
||||||
key: 'notes',
|
|
||||||
align: 'center',
|
|
||||||
width: 120,
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
render(row) {
|
|
||||||
return row.notes || '-'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '剩余体验次数',
|
|
||||||
key: 'remaining_count',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
render(row) {
|
|
||||||
return row.remaining_count || 0
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'actions',
|
|
||||||
width: 200,
|
|
||||||
align: 'center',
|
|
||||||
fixed: 'right',
|
|
||||||
render(row) {
|
|
||||||
return [
|
|
||||||
withDirectives(
|
|
||||||
h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
size: 'small',
|
|
||||||
type: 'info',
|
|
||||||
style: 'margin-right: 8px;',
|
|
||||||
onClick: () => handleViewDetail(row),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () => '详情',
|
|
||||||
icon: renderIcon('material-symbols:info', { size: 16 }),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
[[vPermission, 'get/api/v1/app_user/detail']]
|
|
||||||
),
|
|
||||||
withDirectives(
|
|
||||||
h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
size: 'small',
|
|
||||||
type: 'primary',
|
|
||||||
style: 'margin-right: 8px;',
|
|
||||||
onClick: () => handleSetLimit(row),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () => '次数设置',
|
|
||||||
icon: renderIcon('material-symbols:settings', { size: 16 }),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
[[vPermission, 'post/api/v1/app_user/set_limit']]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// 查看用户详情
|
|
||||||
function handleViewDetail(row) {
|
|
||||||
// 这里可以跳转到详情页面或打开详情弹窗
|
|
||||||
$message.info('查看用户详情功能待实现')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 次数设置
|
|
||||||
function handleSetLimit(row) {
|
|
||||||
currentUser.value = row
|
|
||||||
limitModalVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存次数设置
|
|
||||||
async function handleSaveLimitSetting(data) {
|
|
||||||
try {
|
|
||||||
// 这里调用API保存次数设置
|
|
||||||
// await api.setUserLimit(data)
|
|
||||||
$message.success('次数设置保存成功')
|
|
||||||
limitModalVisible.value = false
|
|
||||||
$table.value?.handleSearch()
|
|
||||||
} catch (error) {
|
|
||||||
$message.error('保存失败: ' + error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateForm = {
|
|
||||||
phone: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请输入手机号',
|
|
||||||
trigger: ['input', 'blur'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
trigger: ['blur'],
|
|
||||||
validator: (rule, value, callback) => {
|
|
||||||
const phoneRegex = /^1[3-9]\d{9}$/
|
|
||||||
if (!phoneRegex.test(value)) {
|
|
||||||
callback('请输入正确的手机号格式')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
callback()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CommonPage show-footer title="用户列表">
|
|
||||||
<template #action>
|
|
||||||
<!-- <NButton v-permission="'post/api/v1/app_user/create'" type="primary" @click="handleAdd">
|
|
||||||
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" />新建用户
|
|
||||||
</NButton> -->
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 表格 -->
|
|
||||||
<CrudTable
|
|
||||||
ref="$table"
|
|
||||||
v-model:query-items="queryItems"
|
|
||||||
:columns="columns"
|
|
||||||
:get-data="api.getAppUserList"
|
|
||||||
>
|
|
||||||
<template #queryBar>
|
|
||||||
<QueryBarItem label="手机号" :label-width="50">
|
|
||||||
<NInput
|
|
||||||
v-model:value="queryItems.phone"
|
|
||||||
clearable
|
|
||||||
type="text"
|
|
||||||
placeholder="请输入手机号"
|
|
||||||
@keypress.enter="$table?.handleSearch()"
|
|
||||||
/>
|
|
||||||
</QueryBarItem>
|
|
||||||
<QueryBarItem label="微信号" :label-width="50">
|
|
||||||
<NInput
|
|
||||||
v-model:value="queryItems.wechat"
|
|
||||||
clearable
|
|
||||||
type="text"
|
|
||||||
placeholder="请输入微信号"
|
|
||||||
@keypress.enter="$table?.handleSearch()"
|
|
||||||
/>
|
|
||||||
</QueryBarItem>
|
|
||||||
</template>
|
|
||||||
</CrudTable>
|
|
||||||
|
|
||||||
<!-- 新增/编辑 弹窗 -->
|
|
||||||
<CrudModal
|
|
||||||
v-model:visible="modalVisible"
|
|
||||||
:title="modalTitle"
|
|
||||||
:loading="modalLoading"
|
|
||||||
@save="handleSave"
|
|
||||||
>
|
|
||||||
<NForm
|
|
||||||
ref="modalFormRef"
|
|
||||||
label-placement="left"
|
|
||||||
label-align="left"
|
|
||||||
:label-width="80"
|
|
||||||
:model="modalForm"
|
|
||||||
:rules="validateForm"
|
|
||||||
>
|
|
||||||
<NFormItem label="手机号" path="phone">
|
|
||||||
<NInput v-model:value="modalForm.phone" clearable placeholder="请输入手机号" />
|
|
||||||
</NFormItem>
|
|
||||||
<NFormItem v-if="modalAction === 'add'" label="说明">
|
|
||||||
<span style="color: #999; font-size: 12px;">
|
|
||||||
注册后默认密码为手机号后6位
|
|
||||||
</span>
|
|
||||||
</NFormItem>
|
|
||||||
</NForm>
|
|
||||||
</CrudModal>
|
|
||||||
|
|
||||||
<!-- 次数设置弹窗 -->
|
|
||||||
<LimitSettingModal
|
|
||||||
v-model:visible="limitModalVisible"
|
|
||||||
:user-data="currentUser"
|
|
||||||
@save="handleSaveLimitSetting"
|
|
||||||
/>
|
|
||||||
</CommonPage>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
@ -1,422 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import {
|
|
||||||
NButton,
|
|
||||||
NTag,
|
|
||||||
NTabs,
|
|
||||||
NTabPane,
|
|
||||||
NSpin,
|
|
||||||
NImage,
|
|
||||||
NImageGroup,
|
|
||||||
} from 'naive-ui'
|
|
||||||
|
|
||||||
import { formatDate } from '@/utils'
|
|
||||||
import TheIcon from '@/components/icon/TheIcon.vue'
|
|
||||||
|
|
||||||
import { getStatusConfig } from '../constants'
|
|
||||||
import {
|
|
||||||
formatAgeDistribution,
|
|
||||||
formatAmount,
|
|
||||||
formatHistoricalEvidence,
|
|
||||||
formatPercent,
|
|
||||||
formatPlatformAccounts,
|
|
||||||
formatPriceRange,
|
|
||||||
formatThreeYearIncome,
|
|
||||||
formatNumberValue,
|
|
||||||
} from '../utils'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
loading: { type: Boolean, default: false },
|
|
||||||
detailData: { type: Object, default: null },
|
|
||||||
mode: { type: String, default: 'view' },
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['back', 'approve', 'reject'])
|
|
||||||
|
|
||||||
const activeDetailTab = ref('audit')
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.detailData?.id,
|
|
||||||
() => {
|
|
||||||
activeDetailTab.value = 'audit'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const detailSections = computed(() => {
|
|
||||||
const detail = props.detailData
|
|
||||||
if (!detail) return []
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
key: 'basic',
|
|
||||||
title: '基础信息',
|
|
||||||
columns: [
|
|
||||||
{ label: '资产名称', type: 'text', value: detail.asset_name || '-' },
|
|
||||||
{ label: '所属机构', type: 'text', value: detail.institution || '-' },
|
|
||||||
{ label: '所属行业', type: 'text', value: detail.industry || '-' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'finance',
|
|
||||||
title: '财务状况',
|
|
||||||
columns: [
|
|
||||||
{ label: '近12个月机构营收/万元', type: 'text', value: formatNumberValue(detail.annual_revenue) },
|
|
||||||
{ label: '近12个月机构研发投入/万元', type: 'text', value: formatNumberValue(detail.rd_investment) },
|
|
||||||
{ label: '近三年机构收益/万元', type: 'list', value: formatThreeYearIncome(detail.three_year_income) },
|
|
||||||
{ label: '资产受资助情况', type: 'text', value: detail.funding_status || '-' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'tech',
|
|
||||||
title: '非遗等级与技术',
|
|
||||||
columns: [
|
|
||||||
{ label: '非遗传承人等级', type: 'text', value: detail.inheritor_level || '-' },
|
|
||||||
{ label: '非遗传承人年龄水平及数量', type: 'list', value: formatAgeDistribution(detail.inheritor_age_count) },
|
|
||||||
{ label: '非遗传承人等级证书', type: 'images', value: detail.inheritor_certificates || [] },
|
|
||||||
{ label: '非遗等级', type: 'text', value: detail.heritage_level || '-' },
|
|
||||||
{ label: '非遗资产所用专利的申请号', type: 'text', value: detail.patent_application_no || '-' },
|
|
||||||
{ label: '非遗资产历史证明证据及数量', type: 'list', value: formatHistoricalEvidence(detail.historical_evidence) },
|
|
||||||
{
|
|
||||||
label: '非遗资产所用专利/纹样图片',
|
|
||||||
type: 'images',
|
|
||||||
value: [...(detail.patent_certificates || []), ...(detail.pattern_images || [])],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'promotion',
|
|
||||||
title: '非遗应用与推广',
|
|
||||||
columns: [
|
|
||||||
{ label: '非遗资产应用成熟度', type: 'text', value: detail.application_maturity || detail.implementation_stage || '-' },
|
|
||||||
{ label: '非遗资产应用覆盖范围', type: 'text', value: detail.application_coverage || detail.coverage_area || '-' },
|
|
||||||
{ label: '非遗资产跨界合作深度', type: 'text', value: detail.cooperation_depth || detail.collaboration_type || '-' },
|
|
||||||
{ label: '近12个月线下相关宣讲活动次数', type: 'text', value: formatNumberValue(detail.offline_activities) },
|
|
||||||
{ label: '线上相关宣传账号信息', type: 'list', value: formatPlatformAccounts(detail.platform_accounts) },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'products',
|
|
||||||
title: '非遗资产衍生商品信息',
|
|
||||||
columns: [
|
|
||||||
{ label: '代表产品近12个月销售数量', type: 'text', value: formatNumberValue(detail.sales_volume) },
|
|
||||||
{ label: '商品链接浏览量', type: 'text', value: formatNumberValue(detail.link_views) },
|
|
||||||
{ label: '发行量', type: 'text', value: detail.circulation || detail.scarcity_level || '-' },
|
|
||||||
{ label: '最近一次市场活动时间', type: 'text', value: detail.last_market_activity || detail.market_activity_time || '-' },
|
|
||||||
{ label: '月交易额水平', type: 'text', value: detail.monthly_transaction || detail.monthly_transaction_amount || '-' },
|
|
||||||
{ label: '近30天价格区间', type: 'text', value: formatPriceRange(detail.price_fluctuation) },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
const calcFlow = computed(() => props.detailData?.calculation_result?.flow || [])
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="audit-detail">
|
|
||||||
<div class="detail-header">
|
|
||||||
<div>
|
|
||||||
<button type="button" class="back-btn" @click="emit('back')">
|
|
||||||
<TheIcon icon="mdi:arrow-left" :size="16" class="mr-4" />
|
|
||||||
返回审核列表
|
|
||||||
</button>
|
|
||||||
<div class="detail-title">
|
|
||||||
<h2>{{ detailData?.asset_name || '审核详情' }}</h2>
|
|
||||||
<NTag size="small" :type="getStatusConfig(detailData?.status).type">
|
|
||||||
{{ getStatusConfig(detailData?.status).text }}
|
|
||||||
</NTag>
|
|
||||||
</div>
|
|
||||||
<p class="detail-meta">
|
|
||||||
<span>手机号:{{ detailData?.phone || '-' }}</span>
|
|
||||||
<span>微信号:{{ detailData?.wechat || '-' }}</span>
|
|
||||||
<span>提交时间:{{ formatDate(detailData?.created_at) }}</span>
|
|
||||||
<span>审核时间:{{ detailData?.reviewed_at ? formatDate(detailData?.reviewed_at) : '-' }}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="mode === 'approve' && detailData?.status === 'pending'" class="detail-actions">
|
|
||||||
<NButton tertiary type="error" @click="emit('reject')">
|
|
||||||
<TheIcon icon="mdi:close-circle-outline" :size="16" class="mr-4" />
|
|
||||||
拒绝
|
|
||||||
</NButton>
|
|
||||||
<NButton type="primary" @click="emit('approve')">
|
|
||||||
<TheIcon icon="mdi:check-circle-outline" :size="16" class="mr-4" />
|
|
||||||
通过
|
|
||||||
</NButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<NTabs v-model:value="activeDetailTab">
|
|
||||||
<NTabPane name="audit" tab="审核信息">
|
|
||||||
<NSpin :show="loading">
|
|
||||||
<div v-for="section in detailSections" :key="section.key" class="detail-section">
|
|
||||||
<div class="section-title">
|
|
||||||
<span class="dot" />
|
|
||||||
<span>{{ section.title }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="table-wrapper">
|
|
||||||
<table class="info-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="first-col">字段名</th>
|
|
||||||
<th v-for="column in section.columns" :key="column.label">
|
|
||||||
{{ column.label }}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="first-col">用户输入</td>
|
|
||||||
<td v-for="column in section.columns" :key="column.label">
|
|
||||||
<div v-if="column.type === 'list'" class="cell-multi">
|
|
||||||
<template v-if="column.value && column.value.length">
|
|
||||||
<span v-for="(item, idx) in column.value" :key="idx">{{ item }}</span>
|
|
||||||
</template>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="column.type === 'images'">
|
|
||||||
<template v-if="column.value && column.value.length">
|
|
||||||
<NImageGroup>
|
|
||||||
<NImage
|
|
||||||
v-for="(img, idx) in column.value"
|
|
||||||
:key="idx"
|
|
||||||
width="72"
|
|
||||||
height="48"
|
|
||||||
:src="img"
|
|
||||||
object-fit="cover"
|
|
||||||
/>
|
|
||||||
</NImageGroup>
|
|
||||||
</template>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</div>
|
|
||||||
<span v-else>{{ column.value || '-' }}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</NSpin>
|
|
||||||
</NTabPane>
|
|
||||||
<NTabPane name="flow" tab="计算流程">
|
|
||||||
<NSpin :show="loading">
|
|
||||||
<div class="calc-summary">
|
|
||||||
<div class="calc-card">
|
|
||||||
<p>模型估值(A·B法)</p>
|
|
||||||
<strong>{{ formatAmount(detailData?.model_value_b) }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="calc-card">
|
|
||||||
<p>市场对标估值</p>
|
|
||||||
<strong>{{ formatAmount(detailData?.market_value_c) }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="calc-card">
|
|
||||||
<p>综合校准估值</p>
|
|
||||||
<strong>{{ formatAmount(detailData?.final_value_ab) }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="calc-card">
|
|
||||||
<p>动态质押率</p>
|
|
||||||
<strong>{{ formatPercent(detailData?.dynamic_pledge_rate) }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="calcFlow.length" class="calc-flow">
|
|
||||||
<div v-for="(step, index) in calcFlow" :key="index" class="calc-step">
|
|
||||||
<div class="step-index">{{ index + 1 }}</div>
|
|
||||||
<div class="step-body">
|
|
||||||
<p class="step-title">{{ step.title }}</p>
|
|
||||||
<p class="step-desc">{{ step.description }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="step-value">{{ step.value }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="calc-empty">暂无计算流程数据</div>
|
|
||||||
</NSpin>
|
|
||||||
</NTabPane>
|
|
||||||
</NTabs>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.audit-detail {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: #409eff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-title h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-meta {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
color: #666;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-section {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title .dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #409eff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
background: #f9fafe;
|
|
||||||
table-layout: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-table th,
|
|
||||||
.info-table td {
|
|
||||||
border: 1px solid #e5e6eb;
|
|
||||||
padding: 12px;
|
|
||||||
text-align: left;
|
|
||||||
min-width: 140px;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-table .first-col {
|
|
||||||
width: 120px;
|
|
||||||
text-align: center;
|
|
||||||
background: #f1f2f5;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-table th:not(.first-col),
|
|
||||||
.info-table td:not(.first-col) {
|
|
||||||
width: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell-multi {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calc-summary {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calc-card {
|
|
||||||
padding: 16px;
|
|
||||||
border: 1px solid #eef0f6;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #fdfdff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calc-card p {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calc-card strong {
|
|
||||||
font-size: 18px;
|
|
||||||
color: #1d2129;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calc-flow {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calc-step {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px dashed #dce1f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-index {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #eef4ff;
|
|
||||||
color: #3b82f6;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-body {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-title {
|
|
||||||
margin: 0 0 6px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-desc {
|
|
||||||
margin: 0;
|
|
||||||
color: #666;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-value {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1d2129;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calc-empty {
|
|
||||||
text-align: center;
|
|
||||||
color: #999;
|
|
||||||
padding: 40px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
export const STATUS_OPTIONS = [
|
|
||||||
{ label: '全部', value: '' },
|
|
||||||
{ label: '待审核', value: 'pending' },
|
|
||||||
{ label: '已完成', value: 'approved' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const STATUS_MAP = {
|
|
||||||
pending: { type: 'warning', text: '待审核' },
|
|
||||||
approved: { type: 'success', text: '已完成' },
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getStatusConfig = (status) => STATUS_MAP[status] || { type: 'default', text: '未知' }
|
|
||||||
@ -1,416 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { h, onMounted, ref, watch } from 'vue'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import {
|
|
||||||
NButton,
|
|
||||||
NForm,
|
|
||||||
NFormItem,
|
|
||||||
NInput,
|
|
||||||
NTag,
|
|
||||||
NSelect,
|
|
||||||
NDatePicker,
|
|
||||||
} from 'naive-ui'
|
|
||||||
|
|
||||||
import CommonPage from '@/components/page/CommonPage.vue'
|
|
||||||
import QueryBarItem from '@/components/query-bar/QueryBarItem.vue'
|
|
||||||
import CrudModal from '@/components/table/CrudModal.vue'
|
|
||||||
import CrudTable from '@/components/table/CrudTable.vue'
|
|
||||||
|
|
||||||
import { formatDate, renderIcon } from '@/utils'
|
|
||||||
import api from '@/api'
|
|
||||||
import TheIcon from '@/components/icon/TheIcon.vue'
|
|
||||||
import AuditDetail from './components/AuditDetail.vue'
|
|
||||||
import { STATUS_OPTIONS, getStatusConfig } from './constants'
|
|
||||||
import { formatAmount } from './utils'
|
|
||||||
|
|
||||||
defineOptions({ name: '审核列表' })
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
|
||||||
const $table = ref(null)
|
|
||||||
const queryItems = ref({})
|
|
||||||
|
|
||||||
// 列表与详情公共方法
|
|
||||||
const renderStatus = (status) => {
|
|
||||||
const config = getStatusConfig(status)
|
|
||||||
return h(NTag, { type: config.type }, { default: () => config.text })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 列定义
|
|
||||||
const columns = [
|
|
||||||
{ title: '编号', key: 'id', width: 80, align: 'center' },
|
|
||||||
{
|
|
||||||
title: '手机号',
|
|
||||||
key: 'phone',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '微信号',
|
|
||||||
key: 'wechat',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
ellipsis: { tooltip: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '评估结果',
|
|
||||||
key: 'valuation_result',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
render(row) {
|
|
||||||
return formatAmount(row.valuation_result)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '提交时间',
|
|
||||||
key: 'created_at',
|
|
||||||
width: 160,
|
|
||||||
align: 'center',
|
|
||||||
render(row) {
|
|
||||||
return formatDate(row.created_at)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '审核时间',
|
|
||||||
key: 'reviewed_at',
|
|
||||||
width: 160,
|
|
||||||
align: 'center',
|
|
||||||
render(row) {
|
|
||||||
return row.reviewed_at ? formatDate(row.reviewed_at) : '-'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
key: 'status',
|
|
||||||
width: 100,
|
|
||||||
align: 'center',
|
|
||||||
render(row) {
|
|
||||||
return renderStatus(row.status)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'actions',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
fixed: 'right',
|
|
||||||
render(row) {
|
|
||||||
if (row.status === 'pending') {
|
|
||||||
return h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
size: 'small',
|
|
||||||
type: 'primary',
|
|
||||||
onClick: () => goToDetail(row, 'approve'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () => '审核',
|
|
||||||
icon: renderIcon('mdi:check-circle-outline', { size: 16 }),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (row.status === 'approved') {
|
|
||||||
return h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
size: 'small',
|
|
||||||
type: 'info',
|
|
||||||
onClick: () => goToDetail(row, 'view'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () => '查看',
|
|
||||||
icon: renderIcon('mdi:eye-outline', { size: 16 }),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// 详情页状态
|
|
||||||
const isDetailView = ref(false)
|
|
||||||
const detailLoading = ref(false)
|
|
||||||
const detailData = ref(null)
|
|
||||||
const detailMode = ref('view')
|
|
||||||
|
|
||||||
// 审核备注弹窗
|
|
||||||
const approvalModalVisible = ref(false)
|
|
||||||
const approvalModalTitle = ref('审核估值评估')
|
|
||||||
const approvalForm = ref({
|
|
||||||
valuation_id: null,
|
|
||||||
admin_notes: '',
|
|
||||||
action: 'approve',
|
|
||||||
})
|
|
||||||
const approvalFormRef = ref(null)
|
|
||||||
|
|
||||||
// 文案设置弹窗
|
|
||||||
const contentModalVisible = ref(false)
|
|
||||||
const contentForm = ref({
|
|
||||||
content: '',
|
|
||||||
})
|
|
||||||
const contentFormRef = ref(null)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
$table.value?.handleSearch()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 格式化金额
|
|
||||||
// 查看详情
|
|
||||||
function goToDetail(row, mode = 'view') {
|
|
||||||
const nextQuery = { ...route.query, detailId: String(row.id), mode }
|
|
||||||
router.push({ query: nextQuery })
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetDetailState() {
|
|
||||||
isDetailView.value = false
|
|
||||||
detailLoading.value = false
|
|
||||||
detailData.value = null
|
|
||||||
detailMode.value = 'view'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadDetail(detailId, mode = 'view') {
|
|
||||||
const parsedId = Number(detailId)
|
|
||||||
if (!parsedId) {
|
|
||||||
resetDetailState()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
detailMode.value = mode === 'approve' ? 'approve' : 'view'
|
|
||||||
isDetailView.value = true
|
|
||||||
detailLoading.value = true
|
|
||||||
try {
|
|
||||||
const { data } = await api.getValuationById({ valuation_id: parsedId })
|
|
||||||
detailData.value = data
|
|
||||||
approvalForm.value.valuation_id = parsedId
|
|
||||||
} catch (error) {
|
|
||||||
$message?.error(error?.message || '获取详情失败')
|
|
||||||
backToList()
|
|
||||||
} finally {
|
|
||||||
detailLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function backToList() {
|
|
||||||
const nextQuery = { ...route.query }
|
|
||||||
delete nextQuery.detailId
|
|
||||||
delete nextQuery.mode
|
|
||||||
router.replace({ query: nextQuery })
|
|
||||||
$table.value?.handleSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
function openApprovalModal(action) {
|
|
||||||
approvalForm.value.action = action
|
|
||||||
approvalForm.value.admin_notes = ''
|
|
||||||
approvalModalTitle.value = action === 'approve' ? '通过审核' : '拒绝审核'
|
|
||||||
approvalModalVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文案设置弹窗
|
|
||||||
function handleAddContent() {
|
|
||||||
contentForm.value = { content: '' }
|
|
||||||
contentModalVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleContentSave() {
|
|
||||||
contentFormRef.value?.validate((errors) => {
|
|
||||||
if (!errors) {
|
|
||||||
contentModalVisible.value = false
|
|
||||||
$message?.success('文案上传并通知成功')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 审核处理
|
|
||||||
async function handleApprovalSubmit() {
|
|
||||||
try {
|
|
||||||
await approvalFormRef.value?.validate()
|
|
||||||
const action = approvalForm.value.action
|
|
||||||
const apiCall = action === 'approve' ? api.approveValuation : api.rejectValuation
|
|
||||||
await apiCall({
|
|
||||||
valuation_id: approvalForm.value.valuation_id,
|
|
||||||
admin_notes: approvalForm.value.admin_notes,
|
|
||||||
})
|
|
||||||
$message?.success(action === 'approve' ? '审核通过成功' : '审核拒绝成功')
|
|
||||||
approvalModalVisible.value = false
|
|
||||||
backToList()
|
|
||||||
} catch (error) {
|
|
||||||
if (error?.message) {
|
|
||||||
$message?.error(error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const approvalRules = {
|
|
||||||
admin_notes: [
|
|
||||||
{ required: true, message: '请输入审核备注', trigger: ['input', 'blur'] },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentRules = {
|
|
||||||
content: [
|
|
||||||
{ required: true, message: '请输入文案内容', trigger: ['input', 'blur'] },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [route.query.detailId, route.query.mode],
|
|
||||||
([detailId, mode]) => {
|
|
||||||
if (detailId) {
|
|
||||||
loadDetail(detailId, mode)
|
|
||||||
} else {
|
|
||||||
resetDetailState()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CommonPage show-footer title="审核列表">
|
|
||||||
<template v-if="!isDetailView">
|
|
||||||
<CrudTable
|
|
||||||
ref="$table"
|
|
||||||
v-model:query-items="queryItems"
|
|
||||||
:columns="columns"
|
|
||||||
:get-data="api.getValuationList"
|
|
||||||
>
|
|
||||||
<template #queryBar>
|
|
||||||
<QueryBarItem label="手机号" :label-width="80">
|
|
||||||
<NInput
|
|
||||||
v-model:value="queryItems.phone"
|
|
||||||
clearable
|
|
||||||
type="text"
|
|
||||||
placeholder="请输入手机号"
|
|
||||||
style="width: 200px"
|
|
||||||
@keypress.enter="$table?.handleSearch()"
|
|
||||||
/>
|
|
||||||
</QueryBarItem>
|
|
||||||
<QueryBarItem label="微信号" :label-width="80">
|
|
||||||
<NInput
|
|
||||||
v-model:value="queryItems.wechat"
|
|
||||||
clearable
|
|
||||||
type="text"
|
|
||||||
placeholder="请输入微信号"
|
|
||||||
style="width: 200px"
|
|
||||||
@keypress.enter="$table?.handleSearch()"
|
|
||||||
/>
|
|
||||||
</QueryBarItem>
|
|
||||||
<QueryBarItem label="提交时间" :label-width="80">
|
|
||||||
<NDatePicker
|
|
||||||
v-model:value="queryItems.created_at"
|
|
||||||
type="daterange"
|
|
||||||
clearable
|
|
||||||
placeholder="请选择提交时间"
|
|
||||||
style="width: 280px"
|
|
||||||
@update:value="$table?.handleSearch()"
|
|
||||||
/>
|
|
||||||
</QueryBarItem>
|
|
||||||
<QueryBarItem label="审核时间" :label-width="80">
|
|
||||||
<NDatePicker
|
|
||||||
v-model:value="queryItems.reviewed_at"
|
|
||||||
type="daterange"
|
|
||||||
clearable
|
|
||||||
placeholder="请选择审核时间"
|
|
||||||
style="width: 280px"
|
|
||||||
@update:value="$table?.handleSearch()"
|
|
||||||
/>
|
|
||||||
</QueryBarItem>
|
|
||||||
<QueryBarItem label="状态" :label-width="80">
|
|
||||||
<NSelect
|
|
||||||
v-model:value="queryItems.status"
|
|
||||||
:options="STATUS_OPTIONS"
|
|
||||||
placeholder="请选择状态"
|
|
||||||
clearable
|
|
||||||
style="width: 200px"
|
|
||||||
@update:value="$table?.handleSearch()"
|
|
||||||
/>
|
|
||||||
</QueryBarItem>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #action>
|
|
||||||
<NButton type="primary" @click="handleAddContent">
|
|
||||||
<TheIcon icon="mdi:plus" :size="18" class="mr-5" />
|
|
||||||
新增文案设置
|
|
||||||
</NButton>
|
|
||||||
</template>
|
|
||||||
</CrudTable>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<AuditDetail
|
|
||||||
:detail-data="detailData"
|
|
||||||
:loading="detailLoading"
|
|
||||||
:mode="detailMode"
|
|
||||||
@back="backToList"
|
|
||||||
@approve="openApprovalModal('approve')"
|
|
||||||
@reject="openApprovalModal('reject')"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 审核弹窗 -->
|
|
||||||
<CrudModal v-model:visible="approvalModalVisible" :title="approvalModalTitle" :show-footer="false">
|
|
||||||
<NForm
|
|
||||||
ref="approvalFormRef"
|
|
||||||
label-placement="left"
|
|
||||||
label-align="left"
|
|
||||||
:label-width="100"
|
|
||||||
:model="approvalForm"
|
|
||||||
:rules="approvalRules"
|
|
||||||
>
|
|
||||||
<NFormItem label="审核备注" path="admin_notes">
|
|
||||||
<NInput
|
|
||||||
v-model:value="approvalForm.admin_notes"
|
|
||||||
type="textarea"
|
|
||||||
:rows="4"
|
|
||||||
placeholder="请输入审核备注"
|
|
||||||
/>
|
|
||||||
</NFormItem>
|
|
||||||
<NFormItem>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<NButton @click="approvalModalVisible = false">取消</NButton>
|
|
||||||
<NButton type="primary" @click="handleApprovalSubmit">
|
|
||||||
{{ approvalForm.action === 'approve' ? '确认通过' : '确认拒绝' }}
|
|
||||||
</NButton>
|
|
||||||
</div>
|
|
||||||
</NFormItem>
|
|
||||||
</NForm>
|
|
||||||
</CrudModal>
|
|
||||||
|
|
||||||
<!-- 文案设置弹窗 -->
|
|
||||||
<CrudModal v-model:visible="contentModalVisible" title="新增文案设置" :show-footer="false">
|
|
||||||
<NForm
|
|
||||||
ref="contentFormRef"
|
|
||||||
label-placement="left"
|
|
||||||
label-align="left"
|
|
||||||
:label-width="80"
|
|
||||||
:model="contentForm"
|
|
||||||
:rules="contentRules"
|
|
||||||
>
|
|
||||||
<NFormItem label="文案" path="content">
|
|
||||||
<NInput
|
|
||||||
v-model:value="contentForm.content"
|
|
||||||
type="textarea"
|
|
||||||
:rows="6"
|
|
||||||
placeholder="请输入文案内容"
|
|
||||||
/>
|
|
||||||
</NFormItem>
|
|
||||||
<NFormItem>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<NButton @click="contentModalVisible = false">取消</NButton>
|
|
||||||
<NButton type="primary" @click="handleContentSave">上传并通知</NButton>
|
|
||||||
</div>
|
|
||||||
</NFormItem>
|
|
||||||
</NForm>
|
|
||||||
</CrudModal>
|
|
||||||
</CommonPage>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.modal-actions {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
const platformLabelMap = {
|
|
||||||
bilibili: 'B站账号',
|
|
||||||
douyin: '抖音账号',
|
|
||||||
kuaishou: '快手账号',
|
|
||||||
qita: '其他账号',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatAmount = (amount) => {
|
|
||||||
if (!amount && amount !== 0) return '-'
|
|
||||||
return `¥${Number(amount).toLocaleString('zh-CN', {
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
})}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatNumberValue = (value, decimals = 0) => {
|
|
||||||
if (value === null || value === undefined || value === '') return '-'
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
return Number(value).toLocaleString('zh-CN', {
|
|
||||||
minimumFractionDigits: decimals,
|
|
||||||
maximumFractionDigits: decimals,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatPercent = (value) => {
|
|
||||||
if (value === null || value === undefined || Number.isNaN(Number(value))) return '-'
|
|
||||||
return `${(Number(value) * 100).toFixed(2)}%`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatThreeYearIncome = (list = []) => {
|
|
||||||
if (!Array.isArray(list) || !list.length) return ['暂无数据']
|
|
||||||
return list.map((item, index) => `第${index + 1}年:${formatNumberValue(item)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatAgeDistribution = (list = []) => {
|
|
||||||
return [
|
|
||||||
{ label: '≤50岁', value: list?.[0] },
|
|
||||||
{ label: '50-70岁', value: list?.[1] },
|
|
||||||
{ label: '≥70岁', value: list?.[2] },
|
|
||||||
].map((bucket) => `${bucket.label}:${formatNumberValue(bucket.value)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatHistoricalEvidence = (evidence = {}) => {
|
|
||||||
const mapping = [
|
|
||||||
{ key: 'artifacts', label: '出土实物' },
|
|
||||||
{ key: 'ancient_literature', label: '古代文献' },
|
|
||||||
{ key: 'inheritor_testimony', label: '传承人佐证' },
|
|
||||||
{ key: 'modern_research', label: '现代研究' },
|
|
||||||
]
|
|
||||||
return mapping.map(({ key, label }) => `${label}:${formatNumberValue(evidence?.[key])}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatPlatformAccounts = (accounts = {}) => {
|
|
||||||
const list = Object.entries(accounts || {}).map(([platform, info]) => {
|
|
||||||
const label = platformLabelMap[platform] || platform
|
|
||||||
if (!info) return `${label}:-`
|
|
||||||
return `${label}:${info.account || '-'}(赞${formatNumberValue(info.likes)} / 评${formatNumberValue(
|
|
||||||
info.comments
|
|
||||||
)} / 转${formatNumberValue(info.shares)})`
|
|
||||||
})
|
|
||||||
return list.length ? list : ['暂无账号信息']
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatPriceRange = (range = []) => {
|
|
||||||
if (!Array.isArray(range) || range.length < 2) return '-'
|
|
||||||
return `${formatAmount(range[0])} - ${formatAmount(range[1])}`
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user