Merge remote-tracking branch 'origin/dev/2025b'

# Conflicts:
#	src/main.jsx
main
LiaoYijun 2 months ago
commit 97e997d95e

@ -33,6 +33,7 @@ antd https://ant-design.antgroup.com/components/upload-cn#uploadfile
wps的文档预览 https://wwo.wps.cn/docs/front-end/introduction/quick-start wps的文档预览 https://wwo.wps.cn/docs/front-end/introduction/quick-start
pdf生成 https://github.com/ivmarcos/react-to-pdf pdf生成 https://github.com/ivmarcos/react-to-pdf
react-pdf https://react-pdf.org react-pdf https://react-pdf.org
生成Docx文档 https://docx.js.org/#/?id=welcome
## 阿里云OSS ## 阿里云OSS
Bucket 名称global-highlights-hub Bucket 名称global-highlights-hub

@ -1,3 +1,5 @@
use Tourmanager
CREATE TABLE auth_role CREATE TABLE auth_role
( (
[role_id] [int] IDENTITY(1,1) NOT NULL, [role_id] [int] IDENTITY(1,1) NOT NULL,
@ -87,6 +89,8 @@ INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('产品管理(客服)', 'route=/products', 'page') VALUES ('产品管理(客服)', 'route=/products', 'page')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('产品管理(供应商)', 'route=/products/edit', 'page') VALUES ('产品管理(供应商)', 'route=/products/edit', 'page')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('采购年份', 'route=/products/pick-year', 'page')
INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id]) INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id])
VALUES (1, 1) VALUES (1, 1)

@ -22,8 +22,8 @@
"i18next-http-backend": "^2.5.2", "i18next-http-backend": "^2.5.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^6.30.1",
"react-i18next": "^14.1.2", "react-i18next": "^14.1.2",
"react-router-dom": "^6.30.1",
"react-to-pdf": "^1.0.1", "react-to-pdf": "^1.0.1",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz", "xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz",
"zustand": "^4.5.7" "zustand": "^4.5.7"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

@ -36,6 +36,8 @@
"Table": { "Table": {
"Total": "Total {{total}} items" "Total": "Total {{total}} items"
}, },
"operator": "Operator",
"time": "Time",
"Login": "Login", "Login": "Login",
"Username": "Username", "Username": "Username",
"Realname": "Realname", "Realname": "Realname",
@ -105,4 +107,4 @@
"Finance_Dept_arrproved": "Finance Dept arrproved", "Finance_Dept_arrproved": "Finance Dept arrproved",
"Paid": "Paid" "Paid": "Paid"
} }
} }

@ -1,5 +1,9 @@
{ {
"ProductType": "Product Type", "ProductType": "Product Type",
"ProductName": "Product Name",
"ContractRemarks": "合同备注",
"versionHistory": "Version History",
"versionPublished": "Published",
"type": { "type": {
"Experience": "Experience", "Experience": "Experience",
"Car": "Transport Services", "Car": "Transport Services",
@ -49,6 +53,8 @@
"RecommendsRate": "Recommends Rate", "RecommendsRate": "Recommends Rate",
"OpenWeekdays": "Open Weekdays", "OpenWeekdays": "Open Weekdays",
"DisplayToC": "Display To C", "DisplayToC": "Display To C",
"SortOrder": "Sort order",
"subTypeD": "Package Type",
"Dept": "Dept", "Dept": "Dept",
"Code": "Code", "Code": "Code",
"City": "City", "City": "City",
@ -79,7 +85,8 @@
"withQuote": "Whether to copy the quotation", "withQuote": "Whether to copy the quotation",
"requiredVendor": "Please pick a target vendor", "requiredVendor": "Please pick a target vendor",
"requiredTypes": "Please select product types", "requiredTypes": "Please select product types",
"requiredDept": "Please pick a owner department" "requiredDept": "Please pick a owner department",
"copyTo": "Copy to"
}, },
"Validation": { "Validation": {
"adultPrice": "请输入成人价", "adultPrice": "请输入成人价",

@ -36,6 +36,8 @@
"Table": { "Table": {
"Total": "共 {{total}} 条" "Total": "共 {{total}} 条"
}, },
"operator": "操作",
"time": "时间",
"Login": "登录", "Login": "登录",
"Username": "账号", "Username": "账号",
"Realname": "姓名", "Realname": "姓名",
@ -105,4 +107,4 @@
"Finance_Dept_arrproved": "财务已审核", "Finance_Dept_arrproved": "财务已审核",
"Paid": "已打款" "Paid": "已打款"
} }
} }

@ -1,6 +1,8 @@
{ {
"ProductType": "项目类型", "ProductType": "项目类型",
"ContractRemarks": "合同备注", "ContractRemarks": "合同备注",
"versionHistory": "查看历史",
"versionPublished": "已发布的",
"type": { "type": {
"Experience": "综费", "Experience": "综费",
"Car": "车费", "Car": "车费",
@ -50,6 +52,8 @@
"RecommendsRate": "推荐指数", "RecommendsRate": "推荐指数",
"OpenWeekdays": "开放时间", "OpenWeekdays": "开放时间",
"DisplayToC": "报价信显示", "DisplayToC": "报价信显示",
"SortOrder": "排序",
"subTypeD": "包价类型",
"Dept": "小组", "Dept": "小组",
"Code": "简码", "Code": "简码",
"City": "城市", "City": "城市",

@ -0,0 +1,76 @@
import { useState } from "react";
import { Popover, message, FloatButton, Button, Form, Input } from "antd";
import { BugOutlined } from "@ant-design/icons";
import useAuthStore from "@/stores/Auth";
import { uploadPageSpyLog, sendNotify } from "@/pageSpy";
function LogUploader() {
const [open, setOpen] = useState(false);
const hide = () => {
setOpen(false);
};
const handleOpenChange = (newOpen) => {
setOpen(newOpen);
};
const [currentUser] = useAuthStore((s) => [s.currentUser]);
const [messageApi, contextHolder] = message.useMessage();
const [formBug] = Form.useForm();
const popoverContent = (
<Form
layout={"vertical"}
form={formBug}
initialValues={{ problem: '' }}
scrollToFirstError
onFinish={async (values) => {
const success = await uploadPageSpyLog();
messageApi.success("Thanks for the feedback😊");
if (success) {
sendNotify(currentUser?.realname + "说:" + values.problem);
} else {
sendNotify(currentUser?.realname + "上传日志失败");
}
hide();
formBug.setFieldsValue({problem: ''});
}}
>
<Form.Item
name="problem"
label="Need help?"
rules={[{ required: true, message: "Specify issue needing support." }]}
>
<Input.TextArea rows={3} />
</Form.Item>
<Button
type="primary"
htmlType="submit"
color="cyan"
variant="solid"
block
>
Submit
</Button>
</Form>
);
return (
<>
{contextHolder}
<Popover
content={popoverContent}
trigger={["click"]}
placement="topRight"
open={open}
onOpenChange={handleOpenChange}
fresh
destroyOnHidden
>
<FloatButton icon={<BugOutlined />} />
</Popover>
</>
);
}
export default LogUploader;

@ -271,7 +271,7 @@ function getFields(props) {
"agency", // "agency", //
99, 99,
<Form.Item name="agency" label={t("products:Vendor")} {...fieldProps.agency} initialValue={at(props, "initialValue.agency")[0]}> <Form.Item name="agency" label={t("products:Vendor")} {...fieldProps.agency} initialValue={at(props, "initialValue.agency")[0]}>
<VendorSelector {...fieldComProps.agency} /> <VendorSelector maxTagCount={1} {...fieldComProps.agency} />
</Form.Item>, </Form.Item>,
fieldProps?.agency?.col || 6 fieldProps?.agency?.col || 6
), ),

@ -42,7 +42,7 @@ export const PERM_TRAIN_TICKET = '/train-ticket/all'
// 价格管理 // 价格管理
export const PERM_PRODUCTS_MANAGEMENT = '/products/*'; // 管理 export const PERM_PRODUCTS_MANAGEMENT = '/products/*'; // 管理
export const PERM_PRODUCTS_NEW = '/products/new'; // 新增产品 export const PERM_PRODUCTS_NEW = '/products/new'; // 新增产品
export const PERM_PRODUCTS_INFO_AUDIT = '/products/info/audit'; // 信息.审核 export const PERM_PRODUCTS_INFO_AUDIT = '/products/info/audit'; // 信息.审核 @deprecated
export const PERM_PRODUCTS_INFO_PUT = '/products/info/put'; // 信息.录入 export const PERM_PRODUCTS_INFO_PUT = '/products/info/put'; // 信息.录入
export const PERM_PRODUCTS_OFFER_AUDIT = '/products/offer/audit'; // 价格.审核 export const PERM_PRODUCTS_OFFER_AUDIT = '/products/offer/audit'; // 价格.审核
export const PERM_PRODUCTS_OFFER_PUT = '/products/offer/put'; // 价格.录入 export const PERM_PRODUCTS_OFFER_PUT = '/products/offer/put'; // 价格.录入

@ -96,20 +96,22 @@ export const useProductsAuditStatesMapVal = (value) => {
}; };
/** /**
* @ignore *
*/ */
export const useProductsTypesFieldsets = (type) => { export const useProductsTypesFieldsets = (type) => {
const [isPermitted] = useAuthStore((state) => [state.isPermitted]); const [isPermitted] = useAuthStore((state) => [state.isPermitted]);
const infoDefault = [['city'], ['title']]; const infoDefault = [['city', 'city_list'], ['title']];
const infoAdmin = ['title', 'product_title', 'code', 'remarks', 'dept']; // 'display_to_c' const infoAdmin = ['title', 'product_title', 'code', 'remarks', 'dept']; // 'display_to_c'
const infoDisplay = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['display_to_c'] : []; const infoDisplay = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['display_to_c'] : [];
const infoRecDisplay = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['recommends_rate'] : []; const infoRecDisplay = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['recommends_rate'] : [];
const subTypeD = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['sub_type_D'] : [];
const sortOrder = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['sort_order'] : [];
const infoTypesMap = { const infoTypesMap = {
'6': [[...infoDisplay], []], '6': [[...infoDisplay], []],
'B': [['km', ...infoDisplay], []], 'B': [['km', ...infoDisplay], []],
'J': [[...infoRecDisplay, 'duration', ...infoDisplay], ['description']], 'J': [[...infoRecDisplay, 'duration', ...infoDisplay, ...sortOrder], ['description']],
'Q': [[...infoRecDisplay, 'duration', ...infoDisplay], ['description']], 'Q': [[...infoRecDisplay, 'duration', ...infoDisplay, ...sortOrder], ['description']],
'D': [[...infoRecDisplay, 'duration', ...infoDisplay], ['description']], 'D': [[...infoRecDisplay, 'duration', ...infoDisplay, ...subTypeD, ...sortOrder], ['description']],
'7': [[...infoRecDisplay, 'duration', 'open_weekdays', ...infoDisplay], ['description']], '7': [[...infoRecDisplay, 'duration', 'open_weekdays', ...infoDisplay], ['description']],
'R': [[...infoDisplay], ['description']], 'R': [[...infoDisplay], ['description']],
'8': [[...infoDisplay], []], '8': [[...infoDisplay], []],
@ -152,6 +154,10 @@ export const useNewProductRecord = () => {
'create_date': '', 'create_date': '',
'created_by': '', 'created_by': '',
'edit_status': 2, 'edit_status': 2,
'sort_order': '',
'sub_type_D': '', // 包价类型, 值保存在`item_type`字段中
'item_type': '', // 产品子类型的值
'city_list': [],
}, },
lgc_details: [ lgc_details: [
{ {
@ -182,3 +188,24 @@ export const useNewProductRecord = () => {
], ],
}; };
}; };
export const PackageTypes = [
{ key: '35001', value: '35001', label: '飞机接送' },
{ key: '35002', value: '35002', label: '车站接送' },
{ key: '35003', value: '35003', label: '码头接送' },
{ key: '35004', value: '35004', label: '一天游' },
{ key: '35005', value: '35005', label: '半天游' },
{ key: '35006', value: '35006', label: '夜间活动' },
{ key: '35007', value: '35007', label: '大车游' },
{ key: '35008', value: '35008', label: '单车单导' },
{ key: '35009', value: '35009', label: '单租车' },
{ key: '35010', value: '35010', label: '单导游' },
{ key: '35011', value: '35011', label: '火车站接送' },
{ key: '35012', value: '35012', label: '门票预定' },
{ key: '35013', value: '35013', label: '车导费' },
{ key: '35014', value: '35014', label: '其它(餐补等)' },
];
export const formatGroupSize = (min, max) => {
return max === 1000 ? min === 0 ? '不分人等' : `${min}人以上` : `${min}-${max}`;
};

@ -46,6 +46,8 @@ import ProductsManage from '@/views/products/Manage';
import ProductsDetail from '@/views/products/Detail'; import ProductsDetail from '@/views/products/Detail';
import ProductsAudit from '@/views/products/Audit'; import ProductsAudit from '@/views/products/Audit';
import ImageViewer from '@/views/ImageViewer'; import ImageViewer from '@/views/ImageViewer';
import PickYear from './views/products/PickYear'
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA,PERM_TRAIN_TICKET, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT } from '@/config' import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA,PERM_TRAIN_TICKET, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT } from '@/config'
import './i18n' import './i18n'
@ -95,6 +97,7 @@ const initRouter = async () => {
{ path: "products/:travel_agency_id/:use_year/:audit_state/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>}, { path: "products/:travel_agency_id/:use_year/:audit_state/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
{ path: "products/audit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsAudit /></RequireAuth>}, { path: "products/audit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsAudit /></RequireAuth>},
{ path: "products/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>}, { path: "products/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
{ path: "products/pick-year",element: <RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><PickYear /></RequireAuth>},
// //
] ]
}, },

@ -1,5 +1,27 @@
import { loadScript } from '@/utils/commons'; import { loadScript } from '@/utils/commons';
import { PROJECT_NAME, BUILD_VERSION } from '@/config'; import { PROJECT_NAME, BUILD_VERSION } from '@/config';
import { fetchJSON } from '@/utils/request'
import { usingStorage } from "@/hooks/usingStorage";
export const sendNotify = async (message) => {
const { userId, travelAgencyId } = usingStorage();
const notifyUrl = 'https://p9axztuwd7x8a7.mycht.cn/dingtalk/dingtalkwork/SendMDMsgByDingRobotToGroup';
const params = {
groupid: 'cidFtzcIzNwNoiaGU9Q795CIg==',
msgTitle: '有人求助',
msgText: `${message}\\n\\nID: ${userId}, ${travelAgencyId} | ${PROJECT_NAME} (${BUILD_VERSION})`,
};
return fetchJSON(notifyUrl, params).then((json) => {
if (json.errcode === 0) {
console.info('发送通知成功');
} else {
throw new Error(json?.errmsg + ': ' + json.errcode);
}
});
};
export const loadPageSpy = (title) => { export const loadPageSpy = (title) => {
@ -20,19 +42,45 @@ export const loadPageSpy = (title) => {
PageSpy.registerPlugin(p) PageSpy.registerPlugin(p)
}) })
window.$pageSpy = new PageSpy(PageSpyConfig); window.$pageSpy = new PageSpy(PageSpyConfig);
window.onerror = async function (msg, url, lineNo, columnNo, error) {
// 3
const now = Date.now()
await window.$harbor.uploadPeriods({
startTime: now - 3 * 60000,
endTime: now,
remark: `\`onerror\`自动上传. ${msg}`,
})
}
}); });
}; };
export const uploadPageSpyLog = async () => { export const uploadPageSpyLog = async () => {
// window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
if (import.meta.env.DEV) return true;
if (window.$pageSpy) { if (window.$pageSpy) {
await window.$harbor.upload() // { clearCache: true, remark: '' } try {
alert('Success') // await window.$harbor.upload() // { clearCache: true, remark: '' }
// 1 , upload : 413 Payload Too Large
const now = Date.now();
await window.$harbor.uploadPeriods({
startTime: now - 60 * 60000,
endTime: now,
});
return true;
} catch (error) {
return false;
}
} else { } else {
alert('Failure') return false;
} }
} }
/**
* @deprecated
* @outdated
*/
export const PageSpyLog = () => { export const PageSpyLog = () => {
return ( return (
<> <>

@ -37,14 +37,7 @@ export const fetchPermissionListByUserId = async (userId) => {
return errcode !== 0 ? {} : result return errcode !== 0 ? {} : result
} }
// 取消令牌时间过期检测,待删除
async function fetchLastRequet() {
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-CooperateSOA/GetLastReqDate`)
return errcode !== 0 ? {} : result
}
const initialState = { const initialState = {
tokenInterval: null,
loginStatus: 0, loginStatus: 0,
defaltRoute: '', defaltRoute: '',
currentUser: { currentUser: {
@ -125,10 +118,9 @@ const useAuthStore = create(devtools((set, get) => ({
}, },
logout: () => { logout: () => {
const { tokenInterval, currentUser } = get() const { currentUser } = get()
const { clearStorage } = usingStorage() const { clearStorage } = usingStorage()
clearStorage() clearStorage()
clearInterval(tokenInterval)
set(() => ({ set(() => ({
...initialState, ...initialState,
currentUser: { currentUser: {
@ -175,6 +167,16 @@ const useAuthStore = create(devtools((set, get) => ({
}) })
}, },
// 根据某项数据来判断是否有权限
//
// INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
// VALUES ('审核CH直销产品', '[125, 375]', 'data')
//
// const PERM_PRODUCTS_AUDIT_CH = '[125, 375]'
isAllowed: (perm, data) => {
return true
},
}), { name: 'authStore' })) }), { name: 'authStore' }))
export default useAuthStore export default useAuthStore

@ -146,13 +146,29 @@ export const fetchRemarkList = async (params) => {
} }
/** /**
* 获取合同备注 * 保存合同备注
*/ */
export const postRemarkList = async (params) => { export const postRemarkList = async (params) => {
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_memo_add`, params) const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_memo_add`, params)
return { errcode, result, success: errcode === 0 } return { errcode, result, success: errcode === 0 }
} }
/**
* 产品价格快照
*/
export const getPPSnapshotAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_snapshot`, params)
return errcode !== 0 ? [] : result;
}
/**
* 修改产品的类型
*/
export const moveProductTypeAction = async (params) => {
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_move`, params)
return errcode !== 0 ? [] : result;
};
const defaultRemarkList = [ const defaultRemarkList = [
{id: 0, "product_type_id": "6","Memo": ""}, {id: 0, "product_type_id": "6","Memo": ""},
{id: 0, "product_type_id": "B","Memo": ""}, {id: 0, "product_type_id": "B","Memo": ""},
@ -259,7 +275,7 @@ export const useProductsStore = create(
} }
}, },
newEmptyQuotation: () => ({ newEmptyQuotation: (useDates) => ({
id: null, id: null,
adult_cost: 0, adult_cost: 0,
child_cost: 0, child_cost: 0,
@ -267,10 +283,7 @@ export const useProductsStore = create(
unit_id: '0', unit_id: '0',
group_size_min: 1, group_size_min: 1,
group_size_max: 10, group_size_max: 10,
use_dates: [ use_dates: useDates,
dayjs().startOf('M'),
dayjs().endOf('M')
],
weekdayList: [], weekdayList: [],
fresh: true // 标识是否是新记录,新记录才用添加列表 fresh: true // 标识是否是新记录,新记录才用添加列表
}), }),
@ -297,7 +310,7 @@ export const useProductsStore = create(
weekdays: definition.weekend.join(','), weekdays: definition.weekend.join(','),
WPI_SN: editingProduct.info.id, WPI_SN: editingProduct.info.id,
WPP_VEI_SN: activeAgency.travel_agency_id, WPP_VEI_SN: activeAgency.travel_agency_id,
lastedit_changed: '', lastedit_changed: {},
audit_state_id: -1, audit_state_id: -1,
key: generateId(), key: generateId(),
fresh: false fresh: false
@ -328,24 +341,23 @@ export const useProductsStore = create(
if (formValues.fresh) { if (formValues.fresh) {
formValues.key = generateId() formValues.key = generateId()
formValues.lastedit_changed = '' formValues.lastedit_changed = {}
formValues.audit_state_id = -1 // 新增, formValues.audit_state_id = -1 // 新增,
formValues.fresh = false // 添加到列表后就不是新纪录,保存要修改原来记录 formValues.fresh = false // 添加到列表后就不是新纪录,保存要修改原来记录
mergedList = [...quotationList,...[formValues]] mergedList = [...quotationList,...[formValues]]
} else { } else {
mergedList = quotationList.map(prevQuotation => { mergedList = quotationList.map(prevQuotation => {
if (prevQuotation.key === formValues.key) { if (prevQuotation.key === formValues.key) {
const changedList = [] const changedObject = {}
for (const [key, value] of Object.entries(formValues)) { for (const [key, value] of Object.entries(formValues)) {
if (key === 'use_dates' || key === 'id' || key === 'key') continue if (key === 'use_dates' || key === 'id' || key === 'key' || key === 'weekdayList'
|| key === 'WPI_SN' || key === 'WPP_VEI_SN') continue
const preValue = prevQuotation[key] const preValue = prevQuotation[key]
const hasChanged = preValue !== value const hasChanged = preValue !== value
if (hasChanged) { if (hasChanged) {
changedList.push({ changedObject[key] = preValue
[key]: preValue,
})
} }
} }
@ -361,7 +373,7 @@ export const useProductsStore = create(
use_dates_start: formValues.use_dates_start, use_dates_start: formValues.use_dates_start,
use_dates_end: formValues.use_dates_end, use_dates_end: formValues.use_dates_end,
weekdays: formValues.weekdays, weekdays: formValues.weekdays,
lastedit_changed: JSON.stringify(changedList, null, 2) lastedit_changed: changedObject
} }
} else { } else {
return prevQuotation return prevQuotation

@ -1,6 +1,9 @@
import { Outlet, Link, useHref, useNavigate, NavLink } from 'react-router-dom' import { Outlet, Link, useHref, useNavigate, NavLink } from 'react-router-dom'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Layout, Menu, ConfigProvider, theme, Dropdown, message, FloatButton, Space, Row, Col, Badge, App as AntApp } from 'antd' import {
Popover, Layout, Menu, ConfigProvider, theme, Dropdown, message, FloatButton, Space, Row, Col, Badge, App as AntApp,
Button, Form, Input
} from 'antd'
import { DownOutlined } from '@ant-design/icons' import { DownOutlined } from '@ant-design/icons'
import 'antd/dist/reset.css' import 'antd/dist/reset.css'
import AppLogo from '@/assets/highlights_travel_600_550.png' import AppLogo from '@/assets/highlights_travel_600_550.png'
@ -9,7 +12,6 @@ import { useTranslation } from 'react-i18next'
import zhLocale from 'antd/locale/zh_CN' import zhLocale from 'antd/locale/zh_CN'
import enLocale from 'antd/locale/en_US' import enLocale from 'antd/locale/en_US'
import 'dayjs/locale/zh-cn' import 'dayjs/locale/zh-cn'
import { BugOutlined } from "@ant-design/icons"
import ErrorBoundary from '@/components/ErrorBoundary' import ErrorBoundary from '@/components/ErrorBoundary'
import { BUILD_VERSION, PERM_PRODUCTS_OFFER_PUT, PERM_PRODUCTS_INFO_PUT } from '@/config' import { BUILD_VERSION, PERM_PRODUCTS_OFFER_PUT, PERM_PRODUCTS_INFO_PUT } from '@/config'
import useNoticeStore from '@/stores/Notice' import useNoticeStore from '@/stores/Notice'
@ -18,7 +20,7 @@ import { useThemeContext } from '@/stores/ThemeContext'
import { usingStorage } from '@/hooks/usingStorage' import { usingStorage } from '@/hooks/usingStorage'
import { useDefaultLgc } from '@/i18n/LanguageSwitcher' import { useDefaultLgc } from '@/i18n/LanguageSwitcher'
import { appendRequestParams } from '@/utils/request' import { appendRequestParams } from '@/utils/request'
import { uploadPageSpyLog } from '@/pageSpy'; import LogUploader from '@/components/LogUploader'
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT,PERM_TRAIN_TICKET } from '@/config' import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT,PERM_TRAIN_TICKET } from '@/config'
@ -63,26 +65,15 @@ function App() {
appendRequestParams('lgc', language) appendRequestParams('lgc', language)
}, [i18n.language]) }, [i18n.language])
const uploadLog = () => {
if (window.$pageSpy) {
window.$pageSpy.triggerPlugins('onOfflineLog', 'upload')
messageApi.info('Success')
} else {
messageApi.error('Failure')
}
}
// //
const isProductPermitted = isPermitted(PERM_PRODUCTS_MANAGEMENT) || isPermitted(PERM_PRODUCTS_INFO_PUT) const isProductPermitted = isPermitted(PERM_PRODUCTS_MANAGEMENT) || isPermitted(PERM_PRODUCTS_INFO_PUT)
const productLink = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? '/products' : '/products/edit' const productLink = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? '/products' : '/products/pick-year'
return ( return (
<ConfigProvider locale={antdLng} <ConfigProvider locale={antdLng}
theme={{ theme={{
token: { token: {
colorPrimary: colorPrimary, colorPrimary: colorPrimary,
// "sizeStep": 3,
// "sizeUnit": 3,
}, },
algorithm: theme.defaultAlgorithm, algorithm: theme.defaultAlgorithm,
}}> }}>
@ -93,7 +84,7 @@ function App() {
insetInlineEnd: 94, insetInlineEnd: 94,
}} }}
> >
<FloatButton icon={<BugOutlined />} onClick={() => uploadPageSpyLog()} /> <LogUploader />
<FloatButton.BackTop /> <FloatButton.BackTop />
</FloatButton.Group> </FloatButton.Group>
{contextHolder} {contextHolder}

@ -231,7 +231,7 @@ function Management() {
}} }}
title={t('account:detail')} title={t('account:detail')}
open={isAccountModalOpen} onCancel={() => setAccountModalOpen(false)} open={isAccountModalOpen} onCancel={() => setAccountModalOpen(false)}
destroyOnClose destroyOnHidden
forceRender forceRender
modalRender={(dom) => ( modalRender={(dom) => (
<Form <Form

@ -164,7 +164,7 @@ function RoleList() {
}} }}
title={t('account:detail')} title={t('account:detail')}
open={isRoleModalOpen} onCancel={() => setRoleModalOpen(false)} open={isRoleModalOpen} onCancel={() => setRoleModalOpen(false)}
destroyOnClose destroyOnHidden
forceRender forceRender
modalRender={(dom) => ( modalRender={(dom) => (
<Form <Form

@ -12,7 +12,8 @@ import { PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_AUDIT, PERM_PRODUCTS_OFFE
import Header from './Detail/Header'; import Header from './Detail/Header';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { usingStorage } from '@/hooks/usingStorage'; import { usingStorage } from '@/hooks/usingStorage';
import { ClockCircleFilled, ClockCircleOutlined, PlusCircleFilled, PlusCircleOutlined } from '@ant-design/icons'; import { ClockCircleOutlined, PlusCircleFilled } from '@ant-design/icons';
import ProductQuotationLogPopover, { columnsSets } from './Detail/ProductQuotationLogPopover';
const PriceTable = ({ productType, dataSource, refresh }) => { const PriceTable = ({ productType, dataSource, refresh }) => {
const { t } = useTranslation('products'); const { t } = useTranslation('products');
@ -27,6 +28,8 @@ const PriceTable = ({ productType, dataSource, refresh }) => {
// console.log(dataSource); // console.log(dataSource);
const [logOpenPriceRow, setLogOpenPriceRow] = useState(null); // price id
const handleAuditPriceItem = (state, row, rowIndex) => { const handleAuditPriceItem = (state, row, rowIndex) => {
postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id }) postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id })
.then((json) => { .then((json) => {
@ -60,32 +63,39 @@ const PriceTable = ({ productType, dataSource, refresh }) => {
const editedCls = (r.audit_state_id === 0 ) ? '!bg-amber-100' : ''; // , const editedCls = (r.audit_state_id === 0 ) ? '!bg-amber-100' : ''; // ,
const newCls = (r.audit_state_id === -1 ) ? '!bg-sky-100' : ''; // , const newCls = (r.audit_state_id === -1 ) ? '!bg-sky-100' : ''; // ,
const editedCls_ = isNotEmpty(r.lastedit_changed) ? (r.audit_state_id === 0 ? '!bg-red-100' : '!bg-sky-100') : ''; const editedCls_ = isNotEmpty(r.lastedit_changed) ? (r.audit_state_id === 0 ? '!bg-red-100' : '!bg-sky-100') : '';
return [trCls, bigTrCls, newCls, editedCls].join(' '); const lodHighlightCls = (r.id === logOpenPriceRow ) ? '!bg-violet-300 !text-violet-900' : '';
return [trCls, bigTrCls, newCls, editedCls, lodHighlightCls].join(' ');
}; };
const columns = [ const columns = [
{ key: 'title', dataIndex: ['info', 'title'], width: '16rem', title: t('Title'), onCell: (r, index) => ({ rowSpan: r.rowSpan, }), className: 'bg-white', render: (text, r) => {
const title = text || r.lgc_details?.['2']?.title || r.lgc_details?.['1']?.title || '';
const itemLink = isPermitted(PERM_PRODUCTS_OFFER_AUDIT) ? `/products/${travel_agency_id}/${use_year}/${audit_state}/edit` : isPermitted(PERM_PRODUCTS_OFFER_PUT) ? `/products/edit` : '';
return isNotEmpty(itemLink) ? <span onClick={() => setEditingProduct({info: r.info})}><Link to={itemLink} >{title}</Link></span> : title;
} },
// ...(productType === 'B' ? [{ key: 'km', dataIndex: ['info', 'km'], title: t('KM')}] : []),
{ key: 'adult', title: t('AgeType.Adult'), render: (_, { adult_cost, currency, unit_id, unit_name }) => `${adult_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` },
{ key: 'child', title: t('AgeType.Child'), render: (_, { child_cost, currency, unit_id, unit_name }) => `${child_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` },
// {key: 'unit', title: t('Unit'), },
{
key: 'groupSize',
dataIndex: ['group_size_min'],
title: t('group_size'),
render: (_, { group_size_min, group_size_max }) => `${group_size_min} - ${group_size_max}`,
},
{ {
key: 'useDates', key: 'title',
dataIndex: ['use_dates_start'], dataIndex: ['info', 'title'],
title: t('use_dates'), width: '16rem',
render: (_, { use_dates_start, use_dates_end, weekdays }) => `${use_dates_start} ~ ${use_dates_end}`, // + (weekdays ? `, ${t('OnWeekdays')}${weekdays}` : ''), title: t('Title'),
onCell: (r, index) => ({ rowSpan: r.rowSpan }),
className: 'bg-white',
render: (text, r) => {
const title = text || r.lgc_details?.['2']?.title || r.lgc_details?.['1']?.title || '';
const itemLink = isPermitted(PERM_PRODUCTS_OFFER_AUDIT)
? `/products/${travel_agency_id}/${use_year}/${audit_state}/edit`
: isPermitted(PERM_PRODUCTS_OFFER_PUT)
? `/products/edit`
: '';
return (
<div className=''>
{isNotEmpty(itemLink) ? (
<div className='' onClick={() => setEditingProduct({ info: r.info })}>
<Link to={itemLink}>{title}</Link>
</div>
) : (
title
)}
</div>
);
},
}, },
{ key: 'weekdays', dataIndex: ['weekdays'], title: t('Weekdays'), render: (text, r) => text || t('Unlimited') }, ...columnsSets(t),
{ {
key: 'state', key: 'state',
title: t('State'), title: t('State'),
@ -98,17 +108,59 @@ const PriceTable = ({ productType, dataSource, refresh }) => {
title: '', title: '',
key: 'action', key: 'action',
render: (_, r, ri) => render: (_, r, ri) =>
(Number(r.audit_state_id)) === 0 ? ( [-1, 0, 3].includes(Number(r.audit_state_id)) ? (
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}> <>
<Space> <Space className='w-full [&>*:last-child]:ms-auto'>
<Button onClick={() => handleAuditPriceItem('2', r, ri)}></Button> <RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
<Button onClick={() => handleAuditPriceItem('3', r, ri)}></Button> {Number(r.audit_state_id) === 0 && (
<div className='flex gap-2'>
<Button onClick={() => handleAuditPriceItem('2', r, ri)}></Button>
<Button onClick={() => handleAuditPriceItem('3', r, ri)}></Button>
</div>
)}
</RequireAuth>
<ProductQuotationLogPopover
method={'history'}
{...{ travel_agency_id, product_id: r.info.id, price_id: r.id, use_year }}
onOpenChange={(open) => setLogOpenPriceRow(open ? r.id : null)}
/>
</Space> </Space>
</RequireAuth> </>
) : null, ) : null,
}, },
// {
// title: '',
// key: 'action2',
// width: '6rem',
// className: 'bg-white',
// onCell: (r, index) => ({ rowSpan: r.rowSpan }),
// render: (_, r) => {
// const showPublicBtn = null; // r.pendingQuotation ? <Popover title='' trigger={['click']}> <Button size='small' className='ml-2' onClick={() => { }}></Button></Popover> : null;
// const btn2 = r.showPublicBtn ? (
// <ProductQuotationLogPopover
// method={'published'}
// {...{ travel_agency_id, product_id: r.info.id, price_id: r.id, use_year }}
// triggerProps={{ type: 'primary', ghost: true, size: 'small' }}
// placement='bottom'
// className='max-w-[1000px]'
// />
// ) : null;
// return <div className='absolute bottom-2 right-1'>{btn2}</div>;
// },
// },
]; ];
return <Table size={'small'} className='border-collapse' rowHoverable={false} rowClassName={rowStyle} pagination={false} {...{ columns, }} dataSource={renderData} rowKey={(r) => r.id} />; return (
<Table
size={'small'}
className='border-collapse'
rowHoverable={false}
rowClassName={rowStyle}
pagination={false}
{...{ columns }}
dataSource={renderData}
rowKey={(r) => r.id}
/>
);
}; };
/** /**
@ -124,6 +176,7 @@ const TypesPanels = (props) => {
useEffect(() => { useEffect(() => {
// ; , ; , // ; , ; ,
const hasDataTypes = Object.keys(agencyProducts); const hasDataTypes = Object.keys(agencyProducts);
let tempKey = '';
const _show = productsTypes const _show = productsTypes
.filter((kk) => hasDataTypes.includes(kk.value)) .filter((kk) => hasDataTypes.includes(kk.value))
.map((ele) => { .map((ele) => {
@ -132,21 +185,23 @@ const TypesPanels = (props) => {
r.concat( r.concat(
c.quotation.map((q, i) => ({ c.quotation.map((q, i) => ({
...q, ...q,
weekdays: q.weekdays // weekdays: q.weekdays
.split(',') // .split(',')
.filter(Boolean) // .filter(Boolean)
.map((w) => t(`weekdaysShort.${w}`)) // .map((w) => t(`weekdaysShort.${w}`))
.join(', '), // .join(', '),
info: c.info, info: c.info,
lgc_details: c.lgc_details.reduce((rlgc, clgc) => ({...rlgc, [clgc.lgc]: clgc}), {}), lgc_details: c.lgc_details.reduce((rlgc, clgc) => ({...rlgc, [clgc.lgc]: clgc}), {}),
rowSpan: i === 0 ? c.quotation.length : 0, rowSpan: i === 0 ? c.quotation.length : 0,
rowSpanI: [ri, i], rowSpanI: [ri, i],
showPublicBtn: c.quotation.some(q2 => [0, 3].includes(q2.audit_state_id)),
})) }))
), ),
[] []
); );
tempKey = _children.length > 0 && tempKey==='' ? ele.key : tempKey;
const _childrenByState = groupBy(_children, 'audit_state_id'); const _childrenByState = groupBy(_children, 'audit_state_id');
// console.log(_childrenByState); // if (_children.length > 0) console.log('PriceTable\n'+ele.value+'\n', _children)
return { return {
...ele, ...ele,
extra: <Space> extra: <Space>
@ -168,7 +223,7 @@ const TypesPanels = (props) => {
}}); }});
setShowTypes(_show); setShowTypes(_show);
setActiveKey(isEmpty(_show) ? [] : [_show[0].key]); setActiveKey(isEmpty(_show) ? [] : [tempKey]);
return () => {}; return () => {};
}, [productsTypes, agencyProducts]); }, [productsTypes, agencyProducts]);

@ -71,7 +71,7 @@ export const ContractRemarksModal = () => {
open={isRemarksModalOpen} open={isRemarksModalOpen}
onOk={() => onRemarksFinish()} onOk={() => onRemarksFinish()}
onCancel={() => setRemarksModalOpen(false)} onCancel={() => setRemarksModalOpen(false)}
destroyOnClose destroyOnHidden
forceRender forceRender
> >
<Form <Form

@ -264,7 +264,7 @@ const Header = ({ refresh, ...props }) => {
className="px-2" className="px-2"
to={ to={
isPermitted(PERM_PRODUCTS_OFFER_AUDIT) isPermitted(PERM_PRODUCTS_OFFER_AUDIT)
? `/products/${activeAgency.travel_agency_id}/${pickYear}/${audit_state}/edit` ? `/products/${activeAgency.travel_agency_id}/${pickYear}/all/edit`
: `/products/edit` : `/products/edit`
} }
> >

@ -5,7 +5,7 @@ import { useProductsTypesMapVal, useNewProductRecord } from '@/hooks/useProducts
import useProductsStore, { postProductsSaveAction } from '@/stores/Products/Index'; import useProductsStore, { postProductsSaveAction } from '@/stores/Products/Index';
import useAuthStore from '@/stores/Auth'; import useAuthStore from '@/stores/Auth';
import { PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT, PERM_PRODUCTS_INFO_PUT, PERM_PRODUCTS_NEW } from '@/config'; import { PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT, PERM_PRODUCTS_INFO_PUT, PERM_PRODUCTS_NEW } from '@/config';
import { isEmpty, pick } from '@/utils/commons'; import { isEmpty, objectMapper, pick, unique } from '@/utils/commons';
import ProductInfoForm from './ProductInfoForm'; import ProductInfoForm from './ProductInfoForm';
import { usingStorage } from '@/hooks/usingStorage'; import { usingStorage } from '@/hooks/usingStorage';
import Extras from './Extras'; import Extras from './Extras';
@ -50,13 +50,20 @@ const ProductInfo = ({ ...props }) => {
setLgcEdits({}); setLgcEdits({});
setInfoEditStatus(''); setInfoEditStatus('');
setEditKeys([]);
return () => {}; return () => {};
}, [activeAgency, editingProduct]); }, [activeAgency, editingProduct]);
const [infoEditStatus, setInfoEditStatus] = useState(''); const [infoEditStatus, setInfoEditStatus] = useState('');
const [lgcEdits, setLgcEdits] = useState({}); const [lgcEdits, setLgcEdits] = useState({});
const onValuesChange = (changedValues, forms) => { // const [editChanged, setEditChanged] = useState({});
const [editKeys, setEditKeys] = useState([]);
const onValuesChange = (changedValues) => {
// console.log('onValuesChange', changedValues); // console.log('onValuesChange', changedValues);
const changedKeys = objectMapper(changedValues, { 'city': 'city_id', 'dept': 'dept_id', 'product_title': 'title', 'lgc_details_mapped': 'lgc_details'});
setEditKeys(prev => unique([...prev, ...Object.keys(changedKeys)]));
// const preValues = pick(editingProduct.info, editKeys);
if ('product_title' in changedValues) { if ('product_title' in changedValues) {
setInfoEditStatus('2'); setInfoEditStatus('2');
setLgcEdits({...lgcEdits, '2': {'edit_status': '2'}}); setLgcEdits({...lgcEdits, '2': {'edit_status': '2'}});
@ -71,6 +78,10 @@ const ProductInfo = ({ ...props }) => {
const onSave = async (err, values, forms) => { const onSave = async (err, values, forms) => {
values.travel_agency_id = activeAgency.travel_agency_id; values.travel_agency_id = activeAgency.travel_agency_id;
const editChanged = pick(editingProduct.info, editKeys);
(editKeys.includes('lgc_details') ? editChanged.lgc_details = editingProduct.lgc_details.map(l => l.lgc) : false);
// console.log("editKeys pre values", editKeys, editChanged, '\neditingProduct', );
const copyNewProduct = structuredClone(newProductRecord); const copyNewProduct = structuredClone(newProductRecord);
const poster = { const poster = {
// ...(topPerm ? { } : { 'audit_state': -1 }), // : // ...(topPerm ? { } : { 'audit_state': -1 }), // :
@ -78,9 +89,10 @@ const ProductInfo = ({ ...props }) => {
// "created_by": userId, // "created_by": userId,
'travel_agency_id': activeAgency.travel_agency_id, 'travel_agency_id': activeAgency.travel_agency_id,
// "travel_agency_name": "", // "travel_agency_name": "",
// "lastedit_changed": "", 'lastedit_changed': editChanged, // isEmpty(editChanged) ? "" : JSON.stringify(editChanged),
"edit_status": infoEditStatus || editingProduct.info.edit_status, 'edit_status': infoEditStatus || editingProduct.info.edit_status,
}; };
// console.log("ready to post", poster);
const copyFields = pick(editingProduct.info, ['product_type_id']); // 'title', const copyFields = pick(editingProduct.info, ['product_type_id']); // 'title',
const readyToSubInfo = { ...copyNewProduct.info, ...editingProduct.info, title: editingProduct.info.product_title, ...values.info, ...copyFields, ...poster }; const readyToSubInfo = { ...copyNewProduct.info, ...editingProduct.info, title: editingProduct.info.product_title, ...values.info, ...copyFields, ...poster };
// console.log('onSave', editingProduct.info, readyToSubInfo); // console.log('onSave', editingProduct.info, readyToSubInfo);
@ -95,8 +107,9 @@ const ProductInfo = ({ ...props }) => {
} }
} }
// console.log('before save', '\n lgcEdits:', lgcEdits, '\n mergedLgc', mergedLgc); // console.log('before save', readyToSubInfo, '\n lgcEdits:', lgcEdits, '\n mergedLgc', mergedLgc);
// return false; // debug: 0 // return false; // debug: 0
// throw new Error("Test save");
/** 提交保存 */ /** 提交保存 */
setLoading(true); setLoading(true);
const { success, result } = await postProductsSaveAction({ const { success, result } = await postProductsSaveAction({

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import { useWeekdays } from '@/hooks/useDatePresets'; import { useWeekdays } from '@/hooks/useDatePresets';
import DeptSelector from '@/components/DeptSelector'; import DeptSelector from '@/components/DeptSelector';
import CitySelector from '@/components/CitySelector'; import CitySelector from '@/components/CitySelector';
import { useProductsTypesFieldsets } from '@/hooks/useProductsSets'; import { useProductsTypesFieldsets, PackageTypes } from '@/hooks/useProductsSets';
import useProductsStore from '@/stores/Products/Index'; import useProductsStore from '@/stores/Products/Index';
import ProductInfoLgc from './ProductInfoLgc'; import ProductInfoLgc from './ProductInfoLgc';
import ProductInfoQuotation from './ProductInfoQuotation'; import ProductInfoQuotation from './ProductInfoQuotation';
@ -36,12 +36,8 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
const [showSave, setShowSave] = useState(true); const [showSave, setShowSave] = useState(true);
useEffect(() => { useEffect(() => {
form.resetFields(); form.resetFields();
form.setFieldValue('city', editingProduct?.info?.city_id ? { value: editingProduct?.info?.city_id, label: editingProduct?.info?.city_name } : undefined);
form.setFieldValue('dept', { value: editingProduct?.info?.dept_id, label: editingProduct?.info?.dept_name }); form.setFieldsValue(serverData2Form(editingProduct));
const lgc_details_mapped = (editingProduct?.lgc_details || []).reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
form.setFieldValue('lgc_details_mapped', lgc_details_mapped);
form.setFieldValue('quotation', editingProduct?.quotation);
form.setFieldValue('display_to_c', editingProduct.info?.display_to_c || '0');
setPickEditedInfo({ ...pickEditedInfo, product_title: editingProduct?.info?.product_title }); setPickEditedInfo({ ...pickEditedInfo, product_title: editingProduct?.info?.product_title });
setFormEditable(infoEditable || priceEditable); setFormEditable(infoEditable || priceEditable);
@ -54,7 +50,7 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
const onFinish = (values) => { const onFinish = (values) => {
console.log('Received values of form, origin form value: \n', values); console.log('Received values of form, origin form value: \n', values);
const dest = formValuesMapper(values); const dest = formValuesMapper2Server(values);
console.log('form value send to onSubmit:\n', dest); console.log('form value send to onSubmit:\n', dest);
if (typeof onSubmit === 'function') { if (typeof onSubmit === 'function') {
onSubmit(null, dest, values); onSubmit(null, dest, values);
@ -80,7 +76,7 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
} }
}; };
const onIValuesChange = (changedValues, allValues) => { const onIValuesChange = (changedValues, allValues) => {
const dest = formValuesMapper(allValues); const dest = formValuesMapper2Server(allValues);
// console.log('form onValuesChange', Object.keys(changedValues), changedValues); // console.log('form onValuesChange', Object.keys(changedValues), changedValues);
if ('product_title' in changedValues) { if ('product_title' in changedValues) {
const editTitle = (changedValues.product_title); const editTitle = (changedValues.product_title);
@ -103,8 +99,9 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
onFinish={onFinish} onFinish={onFinish}
onValuesChange={onIValuesChange} onValuesChange={onIValuesChange}
// onFieldsChange={onFieldsChange} // onFieldsChange={onFieldsChange}
initialValues={editingProduct?.info} initialValues={{ ...(editingProduct?.info || {}), sub_type_D: editingProduct?.info?.item_type || '' }}
onFinishFailed={onFinishFailed} scrollToFirstError > onFinishFailed={onFinishFailed}
scrollToFirstError>
<Row> <Row>
{getFields({ sort, initialValue: editingProduct?.info, hides, shows, fieldProps, fieldComProps, form, t, dataSets: { weekdays }, editable: infoEditable })} {getFields({ sort, initialValue: editingProduct?.info, hides, shows, fieldProps, fieldComProps, form, t, dataSets: { weekdays }, editable: infoEditable })}
{/* {showSubmit && ( {/* {showSubmit && (
@ -118,7 +115,8 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
)} */} )} */}
</Row> </Row>
{/* <Divider className='my-1' /> */} {/* <Divider className='my-1' /> */}
<Form.Item className='mb-0' <Form.Item
className='mb-0'
name={'lgc_details_mapped'} name={'lgc_details_mapped'}
rules={[ rules={[
() => ({ () => ({
@ -238,6 +236,14 @@ function getFields(props) {
</Form.Item>, </Form.Item>,
fieldProps?.duration?.col || midCol fieldProps?.duration?.col || midCol
), ),
item(
'city_list',
99,
<Form.Item name='city_list' label={t('多城市')} tooltip={t('把产品绑定到多个城市')}>
<CitySelector mode='multiple' maxTagCount={10} {...styleProps} {...editableProps('city_list')} />
</Form.Item>,
fieldProps?.city_list?.col || midCol
),
item( item(
'km', 'km',
99, 99,
@ -251,7 +257,7 @@ function getFields(props) {
99, 99,
<Form.Item name='recommends_rate' label={t('RecommendsRate')} {...fieldProps.recommends_rate} tooltip={t('FormTooltip.RecommendsRate')}> <Form.Item name='recommends_rate' label={t('RecommendsRate')} {...fieldProps.recommends_rate} tooltip={t('FormTooltip.RecommendsRate')}>
{/* <Input placeholder={t('RecommendsRate')} allowClear /> */} {/* <Input placeholder={t('RecommendsRate')} allowClear /> */}
<InputNumber {...styleProps} {...editableProps('recommends_rate')} min={1} max={1000} /> <InputNumber style={{width: '100%'}} {...styleProps} {...editableProps('recommends_rate')} min={1} max={1000} />
{/* <Select {/* <Select
{...styleProps} {...styleProps}
{...editableProps('recommends_rate')} {...editableProps('recommends_rate')}
@ -266,7 +272,15 @@ function getFields(props) {
]} ]}
/> */} /> */}
</Form.Item>, </Form.Item>,
fieldProps?.recommends_rate?.col || midCol fieldProps?.recommends_rate?.col || (props.shows.includes('sort_order') ? midCol/2 : midCol)
),
item(
'sort_order',
99,
<Form.Item name='sort_order' label={t('SortOrder')} {...fieldProps.sort_order} >
<InputNumber style={{width: '100%'}} {...styleProps} {...editableProps('sort_order')} max={1000} />
</Form.Item>,
fieldProps?.sort_order?.col || midCol/2
), ),
item( item(
'display_to_c', 'display_to_c',
@ -306,6 +320,25 @@ function getFields(props) {
</Form.Item>, </Form.Item>,
fieldProps?.display_to_c?.col || midCol fieldProps?.display_to_c?.col || midCol
), ),
item(
'sub_type_D',
99,
<Form.Item
name='sub_type_D'
label={t('subTypeD')}
{...fieldProps.sub_type_D}
rules={[{ required: true }]}
// tooltip={t('FormTooltip.subTypeD')}
>
<Select
labelInValue={false}
options={PackageTypes}
{...styleProps}
{...editableProps('sub_type_D')}
/>
</Form.Item>,
fieldProps?.sub_type_D?.col || midCol
),
item( item(
'open_weekdays', 'open_weekdays',
99, 99,
@ -370,12 +403,35 @@ function getFields(props) {
return children; return children;
} }
const formValuesMapper = (values) => { const serverData2Form = (productItem) => {
const infoForRender = {
city: productItem?.info?.city_id ? { value: productItem?.info?.city_id, label: productItem?.info?.city_name } : undefined,
dept: { value: productItem?.info?.dept_id, label: productItem?.info?.dept_name },
display_to_c: productItem.info?.display_to_c || '0',
city_list: productItem?.info?.city_list ? productItem?.info?.city_list?.map((ele) => ({ value: ele.id, label: ele.name })) : undefined,
sub_type_D: productItem?.info?.item_type || '',
};
const lgc_details_mapped = (productItem?.lgc_details || []).reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
const quotation = productItem?.quotation || [];
return { ...productItem, ...(productItem?.info || {}), ...infoForRender, lgc_details_mapped };
};
const formValuesMapper2Server = (values) => {
const destinationObject = { const destinationObject = {
'city': [ 'city': [
{ key: 'city_id', transform: (value) => value?.value || value?.key || '' }, { key: 'city_id', transform: (value) => value?.value || value?.key || '' },
{ key: 'city_name', transform: (value) => value?.label || '' }, { key: 'city_name', transform: (value) => value?.label || '' },
], ],
'city_list': [
{ key: 'city_list', transform: (value) => {
return value.map(option => {
return {
id: option?.value || option?.key || '',
name: option?.label || ''
}
})
}},
],
'dept': { key: 'dept_id', transform: (value) => (typeof value === 'string' ? value : value?.value || value?.key || '') }, 'dept': { key: 'dept_id', transform: (value) => (typeof value === 'string' ? value : value?.value || value?.key || '') },
'open_weekdays': { key: 'open_weekdays', transform: (value) => (Array.isArray(value) ? value.join(',') : value) }, 'open_weekdays': { key: 'open_weekdays', transform: (value) => (Array.isArray(value) ? value.join(',') : value) },
// 'recommends_rate': { key: 'recommends_rate', transform: (value) => ((typeof value === 'string' || typeof value === 'number') ? value : value?.value || value?.key || '') }, // 'recommends_rate': { key: 'recommends_rate', transform: (value) => ((typeof value === 'string' || typeof value === 'number') ? value : value?.value || value?.key || '') },
@ -416,13 +472,15 @@ const formValuesMapper = (values) => {
}, },
], ],
'product_title': { key: 'title' }, 'product_title': { key: 'title' },
'sub_type_D': { key: 'item_type'},
'sort_order': { key: 'sort_order'},
}; };
let dest = {}; let dest = {};
const { city, dept, product_title, ...omittedValue } = values; const { city, dept, product_title, sub_type_D, ...omittedValue } = values;
dest = { ...omittedValue, ...objectMapper(values, destinationObject) }; dest = { ...omittedValue, ...objectMapper(values, destinationObject) };
for (const key in dest) { for (const key in dest) {
if (Object.prototype.hasOwnProperty.call(dest, key)) { if (Object.prototype.hasOwnProperty.call(dest, key)) {
dest[key] = typeof dest[key] === 'string' ? (dest[key] || '').trim() : dest[key]; dest[key] = typeof dest[key] === 'string' ? (dest[key] || '').trim() : (dest[key] ?? '');
} }
} }
// omit empty // omit empty

@ -1,352 +1,493 @@
import { useState } from 'react' import { useState, useEffect } from "react";
import { Table, Form, Modal, Button, Radio, Input, Flex, Card, InputNumber, Checkbox, DatePicker, Space, App, Tooltip } from 'antd' import {
import { useTranslation } from 'react-i18next' Table,
import { CloseOutlined, StarTwoTone, PlusOutlined, ExclamationCircleFilled, QuestionCircleOutlined } from '@ant-design/icons' Form,
import { useDatePresets } from '@/hooks/useDatePresets' Modal,
import dayjs from 'dayjs' Button,
import useProductsStore from '@/stores/Products/Index' Radio,
import PriceCompactInput from '@/views/products/Detail/PriceCompactInput' Input,
Flex,
Card,
InputNumber,
Checkbox,
DatePicker,
Space,
App,
Tooltip,
} from "antd";
import { useTranslation } from "react-i18next";
import {
CloseOutlined,
StarTwoTone,
PlusOutlined,
ExclamationCircleFilled,
QuestionCircleOutlined,
} from "@ant-design/icons";
import { useDatePresets } from "@/hooks/useDatePresets";
import dayjs from "dayjs";
const { RangePicker } = DatePicker import useProductsStore from "@/stores/Products/Index";
import PriceCompactInput from "@/views/products/Detail/PriceCompactInput";
import { formatGroupSize } from "@/hooks/useProductsSets";
const batchSetupInitialValues = { const { RangePicker } = DatePicker;
'defList': [
//
{
'useDateList': [
{
'useDate': [
dayjs().add(1, 'year').startOf('y'), dayjs().add(1, 'year').endOf('y')
]
}
],
'unitId': '0',
'currency': 'RMB',
'weekend': [
],
'priceList': [
{
'priceInput': {
'numberStart': 1,
'numberEnd': 2,
'audultPrice': 0,
'childrenPrice': 0
}
},
{
'priceInput': {
'numberStart': 3,
'numberEnd': 4,
'audultPrice': 0,
'childrenPrice': 0
}
},
{
'priceInput': {
'numberStart': 5,
'numberEnd': 6,
'audultPrice': 0,
'childrenPrice': 0
}
},
{
'priceInput': {
'numberStart': 7,
'numberEnd': 9,
'audultPrice': 0,
'childrenPrice': 0
}
}
]
},
//
{
'useDateList': [
{
'useDate': [
dayjs().add(1, 'year').subtract(2, 'M').startOf('M'), dayjs().add(1, 'year').endOf('M')
]
}
],
'unitId': '0',
'currency': 'RMB',
'weekend': [
],
'priceList': [
{
'priceInput': {
'numberStart': 1,
'numberEnd': 2,
'audultPrice': 0,
'childrenPrice': 0
}
},
{
'priceInput': {
'numberStart': 3,
'numberEnd': 4,
'audultPrice': 0,
'childrenPrice': 0
}
},
{
'priceInput': {
'numberStart': 5,
'numberEnd': 6,
'audultPrice': 0,
'childrenPrice': 0
}
},
{
'priceInput': {
'numberStart': 7,
'numberEnd': 9,
'audultPrice': 0,
'childrenPrice': 0
}
}
]
}
]
}
const defaultPriceValue = { const defaultPriceValue = {
'priceInput': { priceInput: {
'numberStart': 1, numberStart: 1,
'numberEnd': 2, numberEnd: 2,
'audultPrice': 0, audultPrice: 0,
'childrenPrice': 0 childrenPrice: 0,
} },
} };
const defaultUseDate = { const getYearRange = (year) => [
'useDate': [dayjs().add(1, 'year').startOf('y'), dayjs().add(1, 'year').endOf('y')] dayjs().year(year).startOf("y"),
} dayjs().year(year).endOf("y"),
];
const defaultDefinitionValue = { const generateDefinitionValue = (year) => ({
'useDateList': [defaultUseDate], useDateList: [{ useDate: getYearRange(year) }],
'unitId': '0', unitId: "0",
'currency': 'RMB', currency: "RMB",
'weekend': [], weekend: [],
'priceList': [defaultPriceValue] priceList: [defaultPriceValue],
} });
const ProductInfoQuotation = ({ editable, ...props }) => { const ProductInfoQuotation = ({ editable, ...props }) => {
const { onChange } = props;
const { onChange } = props const { t } = useTranslation();
const { t } = useTranslation() const [
quotationList,
newEmptyQuotation,
appendQuotationList,
saveOrUpdateQuotation,
deleteQuotation,
switchParams,
] = useProductsStore((state) => [
state.quotationList,
state.newEmptyQuotation,
state.appendQuotationList,
state.saveOrUpdateQuotation,
state.deleteQuotation,
state.switchParams,
]);
const batchSetupInitialValues = {
defList: [
//
{
useDateList: [{ useDate: getYearRange(switchParams.use_year) }],
unitId: "0",
currency: "RMB",
weekend: [],
priceList: [
{
priceInput: {
numberStart: 1,
numberEnd: 2,
audultPrice: 0,
childrenPrice: 0,
},
},
{
priceInput: {
numberStart: 3,
numberEnd: 4,
audultPrice: 0,
childrenPrice: 0,
},
},
{
priceInput: {
numberStart: 5,
numberEnd: 6,
audultPrice: 0,
childrenPrice: 0,
},
},
{
priceInput: {
numberStart: 7,
numberEnd: 9,
audultPrice: 0,
childrenPrice: 0,
},
},
],
},
//
{
useDateList: [
{
useDate: [
dayjs().year(switchParams.use_year).subtract(2, "M").startOf("M"),
dayjs().year(switchParams.use_year).endOf("M"),
],
},
],
unitId: "0",
currency: "RMB",
weekend: [],
priceList: [
{
priceInput: {
numberStart: 1,
numberEnd: 2,
audultPrice: 0,
childrenPrice: 0,
},
},
{
priceInput: {
numberStart: 3,
numberEnd: 4,
audultPrice: 0,
childrenPrice: 0,
},
},
{
priceInput: {
numberStart: 5,
numberEnd: 6,
audultPrice: 0,
childrenPrice: 0,
},
},
{
priceInput: {
numberStart: 7,
numberEnd: 9,
audultPrice: 0,
childrenPrice: 0,
},
},
],
},
],
};
const [isQuotationModalOpen, setQuotationModalOpen] = useState(false) const [defaultUseDates, setDefaultUseDates] = useState(
const [isBatchSetupModalOpen, setBatchSetupModalOpen] = useState(false) getYearRange(switchParams.use_year)
const [groupSizeUnlimit, setGroupSizeUnlimit] = useState(false) );
const [groupMaxUnlimit, setGroupMaxUnlimit] = useState(false) const [defaultDefinitionValue, setDefaultDefinitionValue] = useState(
const { modal, notification } = App.useApp() generateDefinitionValue(switchParams.use_year)
const [quotationForm] = Form.useForm() );
const [batchSetupForm] = Form.useForm() const [isQuotationModalOpen, setQuotationModalOpen] = useState(false);
const [isBatchSetupModalOpen, setBatchSetupModalOpen] = useState(false);
const [groupAllSize, setGroupAllSize] = useState(false);
const [groupMaxUnlimit, setGroupMaxUnlimit] = useState(false);
const { modal, notification } = App.useApp();
const [quotationForm] = Form.useForm();
const [batchSetupForm] = Form.useForm();
const datePresets = useDatePresets() const datePresets = useDatePresets();
const [quotationList, newEmptyQuotation, appendQuotationList, saveOrUpdateQuotation, deleteQuotation] = useEffect(() => {
useProductsStore((state) => [state.quotationList, state.newEmptyQuotation, state.appendQuotationList, state.saveOrUpdateQuotation, state.deleteQuotation]) setDefaultUseDates(getYearRange(switchParams.use_year));
setDefaultDefinitionValue(generateDefinitionValue(switchParams.use_year));
}, [switchParams]);
const triggerChange = (changedValue) => { const triggerChange = (changedValue) => {
onChange?.( onChange?.(changedValue);
changedValue };
)
}
const onQuotationSeleted = async (quotation) => { const onQuotationSeleted = async (quotation) => {
// start, end RangePicker // start, end RangePicker
quotation.use_dates = [dayjs(quotation.use_dates_start), dayjs(quotation.use_dates_end)] quotation.use_dates = [
quotation.weekdayList = quotation.weekdays.split(',') dayjs(quotation.use_dates_start),
quotationForm.setFieldsValue(quotation) dayjs(quotation.use_dates_end),
setQuotationModalOpen(true) ];
} quotation.weekdayList = quotation.weekdays.split(",");
quotationForm.setFieldsValue(quotation);
setQuotationModalOpen(true);
};
const onNewQuotation = () => { const onNewQuotation = () => {
const emptyQuotation = newEmptyQuotation() const emptyQuotation = newEmptyQuotation(defaultUseDates);
quotationForm.setFieldsValue(emptyQuotation) quotationForm.setFieldsValue(emptyQuotation);
setQuotationModalOpen(true) setQuotationModalOpen(true);
} };
const onQuotationFinish = (values) => { const onQuotationFinish = (values) => {
const newList = saveOrUpdateQuotation(values) const newList = saveOrUpdateQuotation(values);
triggerChange(newList) triggerChange(newList);
setQuotationModalOpen(false) setQuotationModalOpen(false);
} };
const onBatchSetupFinish = () => { const onBatchSetupFinish = () => {
const defList = batchSetupForm.getFieldsValue().defList const defList = batchSetupForm.getFieldsValue().defList;
const newList = appendQuotationList(defList) const newList = appendQuotationList(defList);
triggerChange(newList) triggerChange(newList);
setBatchSetupModalOpen(false) setBatchSetupModalOpen(false);
} };
const onDeleteQuotation = (quotation) => { const onDeleteQuotation = (quotation) => {
modal.confirm({ modal.confirm({
title: '请确认', title: "请确认",
icon: <ExclamationCircleFilled />, icon: <ExclamationCircleFilled />,
content: '你要删除这条价格吗?', content: "你要删除这条价格吗?",
onOk() { onOk() {
deleteQuotation(quotation) deleteQuotation(quotation).catch((ex) => {
.catch(ex => { notification.error({
notification.error({ message: "Notification",
message: 'Notification', description: ex.message,
description: ex.message, placement: "top",
placement: 'top', duration: 4,
duration: 4, });
}) });
})
}, },
}) });
} };
const quotationColumns = [ const quotationColumns = [
// { title: 'id', dataIndex: 'id', width: 40, className: 'italic text-gray-400' }, // test: 0 // { title: 'id', dataIndex: 'id', width: 40, className: 'italic text-gray-400' }, // test: 0
// { title: 'WPI_SN', dataIndex: 'WPI_SN', width: 40, className: 'italic text-gray-400' }, // test: 0 // { title: 'WPI_SN', dataIndex: 'WPI_SN', width: 40, className: 'italic text-gray-400' }, // test: 0
{ title: t('products:adultPrice'), dataIndex: 'adult_cost', width: '5rem' }, { title: t("products:adultPrice"), dataIndex: "adult_cost", width: "5rem" },
{ title: t('products:childrenPrice'), dataIndex: 'child_cost', width: '5rem' }, {
{ title: t('products:currency'), dataIndex: 'currency', width: '4rem' }, title: t("products:childrenPrice"),
dataIndex: "child_cost",
width: "5rem",
},
{ title: t("products:currency"), dataIndex: "currency", width: "4rem" },
{ {
title: (<>{t('products:unit_name')} <Tooltip placement='top' overlayInnerStyle={{width: '24rem'}} title={t('products:FormTooltip.PriceUnit')}><QuestionCircleOutlined className='text-gray-500' /></Tooltip> </>), title: (
dataIndex: 'unit_id', <>
width: '6rem', {t("products:unit_name")}{" "}
<Tooltip
placement="top"
overlayInnerStyle={{ width: "24rem" }}
title={t("products:FormTooltip.PriceUnit")}
>
<QuestionCircleOutlined className="text-gray-500" />
</Tooltip>{" "}
</>
),
dataIndex: "unit_id",
width: "6rem",
render: (text) => t(`products:PriceUnit.${text}`), // (text === '0' ? '' : text === '1' ? '' : text), render: (text) => t(`products:PriceUnit.${text}`), // (text === '0' ? '' : text === '1' ? '' : text),
}, },
{ {
title: t('products:group_size'), title: t("products:group_size"),
dataIndex: 'group_size', dataIndex: "group_size",
width: '6rem', width: "6rem",
render: (_, record) => `${record.group_size_min}-${record.group_size_max}`, render: (_, record) =>
formatGroupSize(record.group_size_min, record.group_size_max),
}, },
{ {
title: (<>{t('products:use_dates')} <Tooltip placement='top' overlayInnerStyle={{width: '24rem'}} title={t('products:FormTooltip.UseDates')}><QuestionCircleOutlined className='text-gray-500' /></Tooltip> </>), title: (
dataIndex: 'use_dates', <>
{t("products:use_dates")}{" "}
<Tooltip
placement="top"
overlayInnerStyle={{ width: "24rem" }}
title={t("products:FormTooltip.UseDates")}
>
<QuestionCircleOutlined className="text-gray-500" />
</Tooltip>{" "}
</>
),
dataIndex: "use_dates",
// width: '6rem', // width: '6rem',
render: (_, record) => `${record.use_dates_start}-${record.use_dates_end}`, render: (_, record) =>
`${record.use_dates_start}-${record.use_dates_end}`,
}, },
{ title: t('products:Weekdays'), dataIndex: 'weekdays', width: '4rem' }, { title: t("products:Weekdays"), dataIndex: "weekdays", width: "4rem" },
{ {
title: t('products:operation'), title: t("products:operation"),
dataIndex: 'operation', dataIndex: "operation",
width: '10rem', width: "10rem",
render: (_, quotation) => { render: (_, quotation) => {
// const _rowEditable = [-1,3].includes(quotation.audit_state_id); // const _rowEditable = [-1,3].includes(quotation.audit_state_id);
const _rowEditable = true; // test: 0 const _rowEditable = true; // test: 0
return ( return (
<Space> <Space>
<Button type='link' disabled={!_rowEditable} onClick={() => onQuotationSeleted(quotation)}>{t('Edit')}</Button> <Button
<Button type='link' danger disabled={!_rowEditable} onClick={() => onDeleteQuotation(quotation)}>{t('Delete')}</Button> type="link"
disabled={!_rowEditable}
onClick={() => onQuotationSeleted(quotation)}
>
{t("Edit")}
</Button>
<Button
type="link"
danger
disabled={!_rowEditable}
onClick={() => onDeleteQuotation(quotation)}
>
{t("Delete")}
</Button>
</Space> </Space>
) );
}, },
}, },
] ];
return ( return (
<> <>
<h2>{t('products:EditComponents.Quotation')}</h2> <h2>{t("products:EditComponents.Quotation")}</h2>
<Table size='small' <Table
size="small"
bordered bordered
dataSource={quotationList} dataSource={quotationList}
columns={quotationColumns} columns={quotationColumns}
pagination={false} pagination={false}
/> />
{ {editable && (
editable &&
<Space> <Space>
<Button onClick={() => onNewQuotation()} type='primary' ghost style={{ marginTop: 16 }}> <Button
{t('products:addQuotation')} onClick={() => onNewQuotation()}
type="primary"
ghost
style={{ marginTop: 16 }}
>
{t("products:addQuotation")}
</Button> </Button>
<Button onClick={() => setBatchSetupModalOpen(true)} type='primary' ghost style={{ marginTop: 16, marginLeft: 16 }}> <Button
onClick={() => setBatchSetupModalOpen(true)}
type="primary"
ghost
style={{ marginTop: 16, marginLeft: 16 }}
>
批量设置 批量设置
</Button> </Button>
</Space> </Space>
} )}
<Modal <Modal
centered centered
title='批量设置价格' title="批量设置价格"
width={'640px'} width={"640px"}
open={isBatchSetupModalOpen} open={isBatchSetupModalOpen}
onOk={() => onBatchSetupFinish()} onOk={() => onBatchSetupFinish()}
onCancel={() => setBatchSetupModalOpen(false)} onCancel={() => setBatchSetupModalOpen(false)}
destroyOnClose destroyOnHidden
forceRender forceRender
> >
<Form <Form
labelCol={{ span: 3 }} labelCol={{ span: 3 }}
wrapperCol={{ span: 20 }} wrapperCol={{ span: 20 }}
form={batchSetupForm} form={batchSetupForm}
name='batchSetupForm' name="batchSetupForm"
autoComplete='off' autoComplete="off"
initialValues={batchSetupInitialValues} initialValues={batchSetupInitialValues}
> >
<Form.List name='defList'> <Form.List name="defList">
{(fields, { add, remove }) => ( {(fields, { add, remove }) => (
<Flex gap='middle' vertical> <Flex gap="middle" vertical>
{fields.map((field, index) => ( {fields.map((field, index) => (
<Card <Card
size='small' size="small"
title={index == 0 ? '旺季' : index == 1 ? '淡季' : '其他'} title={
index == 0 ? "全年" : index == 1 ? "特殊时间段" : "其他"
}
key={field.key} key={field.key}
extra={index == 0 ? <StarTwoTone twoToneColor='#eb2f96' /> : <CloseOutlined onClick={() => { extra={
remove(field.name) index == 0 ? (
}} />} <StarTwoTone twoToneColor="#eb2f96" />
) : (
<CloseOutlined
onClick={() => {
remove(field.name);
}}
/>
)
}
> >
<Form.Item label='币种' name={[field.name, 'currency']}> <Form.Item label="币种" name={[field.name, "currency"]}>
<Radio.Group> <Radio.Group>
<Radio value='RMB'>RMB</Radio> <Radio value="RMB">RMB</Radio>
<Radio value='USD'>USD</Radio> <Radio value="USD">USD</Radio>
<Radio value='THB'>THB</Radio> <Radio value="THB">THB</Radio>
<Radio value='JPY'>JPY</Radio> <Radio value="JPY">JPY</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item label='类型' name={[field.name, 'unitId']}> <Form.Item label="类型" name={[field.name, "unitId"]}>
<Radio.Group> <Radio.Group>
<Radio value='0'>每人</Radio> <Radio value="0">每人</Radio>
<Radio value='1'>每团</Radio> <Radio value="1">每团</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item label='周末' name={[field.name, 'weekend']}> <Form.Item label="周末" name={[field.name, "weekend"]}>
<Checkbox.Group <Checkbox.Group options={["5", "6", "7"]} />
options={['5', '6', '7']}
/>
</Form.Item> </Form.Item>
<Form.Item label='有效期'> <Form.Item label="有效期">
<Form.List name={[field.name, 'useDateList']}> <Form.List name={[field.name, "useDateList"]}>
{(useDateFieldList, useDateOptList) => ( {(useDateFieldList, useDateOptList) => (
<Flex gap='middle' vertical> <Flex gap="middle" vertical>
{useDateFieldList.map((useDateField, index) => ( {useDateFieldList.map((useDateField, index) => (
<Space key={useDateField.key}> <Space key={useDateField.key}>
<Form.Item noStyle name={[useDateField.name, 'useDate']}> <Form.Item
<RangePicker style={{ width: '100%' }} allowClear={true} inputReadOnly={true} presets={datePresets} placeholder={['From', 'Thru']} /> noStyle
name={[useDateField.name, "useDate"]}
>
<RangePicker
style={{ width: "100%" }}
allowClear={true}
inputReadOnly={true}
presets={datePresets}
placeholder={["From", "Thru"]}
/>
</Form.Item> </Form.Item>
{index == 0 ? <StarTwoTone twoToneColor='#eb2f96' /> : <CloseOutlined onClick={() => useDateOptList.remove(useDateField.name)} />} {index == 0 ? (
<StarTwoTone twoToneColor="#eb2f96" />
) : (
<CloseOutlined
onClick={() =>
useDateOptList.remove(useDateField.name)
}
/>
)}
</Space> </Space>
))} ))}
<Button type='dashed' icon={<PlusOutlined />} onClick={() => useDateOptList.add(defaultUseDate)} block> <Button
type="dashed"
icon={<PlusOutlined />}
onClick={() =>
useDateOptList.add({ useDate: defaultUseDates })
}
block
>
新增有效期 新增有效期
</Button> </Button>
</Flex> </Flex>
)} )}
</Form.List> </Form.List>
</Form.Item> </Form.Item>
<Form.Item label='人等'> <Form.Item label="人等">
<Form.List name={[field.name, 'priceList']}> <Form.List name={[field.name, "priceList"]}>
{(priceFieldList, priceOptList) => ( {(priceFieldList, priceOptList) => (
<Flex gap='middle' vertical> <Flex gap="middle" vertical>
{priceFieldList.map((priceField, index) => ( {priceFieldList.map((priceField, index) => (
<Space key={priceField.key}> <Space key={priceField.key}>
<Form.Item noStyle name={[priceField.name, 'priceInput']}> <Form.Item
noStyle
name={[priceField.name, "priceInput"]}
>
<PriceCompactInput /> <PriceCompactInput />
</Form.Item> </Form.Item>
{index == 0 ? <StarTwoTone twoToneColor='#eb2f96' /> : <CloseOutlined onClick={() => priceOptList.remove(priceField.name)} />} {index == 0 ? (
<StarTwoTone twoToneColor="#eb2f96" />
) : (
<CloseOutlined
onClick={() =>
priceOptList.remove(priceField.name)
}
/>
)}
</Space> </Space>
))} ))}
<Button type='dashed' icon={<PlusOutlined />} onClick={() => priceOptList.add(defaultPriceValue)} block> <Button
type="dashed"
icon={<PlusOutlined />}
onClick={() =>
priceOptList.add(defaultPriceValue)
}
block
>
新增人等 新增人等
</Button> </Button>
</Flex> </Flex>
@ -355,7 +496,12 @@ const ProductInfoQuotation = ({ editable, ...props }) => {
</Form.Item> </Form.Item>
</Card> </Card>
))} ))}
<Button type='dashed' icon={<PlusOutlined />} onClick={() => add(defaultDefinitionValue)} block> <Button
type="dashed"
icon={<PlusOutlined />}
onClick={() => add(defaultDefinitionValue)}
block
>
新增设置 新增设置
</Button> </Button>
</Flex> </Flex>
@ -368,152 +514,180 @@ const ProductInfoQuotation = ({ editable, ...props }) => {
centered centered
okButtonProps={{ okButtonProps={{
autoFocus: true, autoFocus: true,
htmlType: 'submit', htmlType: "submit",
}} }}
title={t('account:detail')} title={t("products:EditComponents.Quotation")}
open={isQuotationModalOpen} onCancel={() => setQuotationModalOpen(false)} open={isQuotationModalOpen}
destroyOnClose onCancel={() => setQuotationModalOpen(false)}
destroyOnHidden
forceRender forceRender
modalRender={(dom) => ( modalRender={(dom) => (
<Form <Form
name='quotationForm' name="quotationForm"
form={quotationForm} form={quotationForm}
labelCol={{ span: 4 }} labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }} wrapperCol={{ span: 20 }}
className='max-w-2xl' className="max-w-2xl"
onFinish={onQuotationFinish} onFinish={onQuotationFinish}
autoComplete='off' autoComplete="off"
> >
{dom} {dom}
</Form> </Form>
)} )}
> >
<Form.Item name='id' className='hidden' ><Input /></Form.Item> <Form.Item name="id" className="hidden">
<Form.Item name='key' className='hidden' ><Input /></Form.Item> <Input />
<Form.Item name='fresh' className='hidden' ><Input /></Form.Item> </Form.Item>
<Form.Item name="key" className="hidden">
<Input />
</Form.Item>
<Form.Item name="fresh" className="hidden">
<Input />
</Form.Item>
<Form.Item <Form.Item
label={t('products:adultPrice')} label={t("products:adultPrice")}
name='adult_cost' name="adult_cost"
rules={[ rules={[
{ {
required: true, required: true,
message: t('products:Validation.adultPrice'), message: t("products:Validation.adultPrice"),
}, },
]} ]}
> >
<InputNumber style={{ width: '100%' }} /> <InputNumber style={{ width: "100%" }} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('products:childrenPrice')} label={t("products:childrenPrice")}
name='child_cost' name="child_cost"
rules={[ rules={[
{ {
required: true, required: true,
message: t('products:Validation.childrenPrice'), message: t("products:Validation.childrenPrice"),
}, },
]} ]}
> >
<InputNumber style={{ width: '100%' }} /> <InputNumber style={{ width: "100%" }} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('products:currency')} label={t("products:currency")}
name='currency' name="currency"
rules={[ rules={[
{ {
required: true, required: true,
message: t('products:Validation.currency'), message: t("products:Validation.currency"),
}, },
]} ]}
> >
<Radio.Group> <Radio.Group>
<Radio value='RMB'>RMB</Radio> <Radio value="RMB">RMB</Radio>
<Radio value='USD'>USD</Radio> <Radio value="USD">USD</Radio>
<Radio value='THB'>THB</Radio> <Radio value="THB">THB</Radio>
<Radio value='JPY'>JPY</Radio> <Radio value="JPY">JPY</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('products:unit_name')} label={t("products:unit_name")}
name='unit_id' name="unit_id"
rules={[ rules={[
{ {
required: true, required: true,
message: t('products:Validation.unit_name'), message: t("products:Validation.unit_name"),
}, },
]} ]}
> >
<Radio.Group> <Radio.Group>
<Radio value='0'>每人</Radio> <Radio value="0">每人</Radio>
<Radio value='1'>每团</Radio> <Radio value="1">每团</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Checkbox onChange={e => { <Checkbox
onChange={(e) => {
if (e.target.checked) { if (e.target.checked) {
quotationForm.setFieldValue('group_size_min', 0) quotationForm.setFieldValue("group_size_min", 1);
quotationForm.setFieldValue('group_size_max', 1000) quotationForm.setFieldValue("group_size_max", 1000);
setGroupSizeUnlimit(true) setGroupAllSize(true);
} else { } else {
quotationForm.setFieldValue('group_size_min', 1) setGroupAllSize(false);
if (!groupMaxUnlimit) quotationForm.setFieldValue('group_size_max', 999)
setGroupSizeUnlimit(false)
} }
}}>不分人等(0~1000)</Checkbox> }}
>
<span className="font-bold">不分人等(1~1000)</span>
</Checkbox>
<Form.Item <Form.Item
label={t('products:group_size')} label={t("products:group_size")}
name='group_size_min' name="group_size_min"
rules={[ rules={[
{ {
required: true, required: true,
message: t('products:Validation.group_size_min'), message: t("products:Validation.group_size_min"),
},
{
validator: (_, value) => {
if (value > 1000 || value < 1) {
return Promise.reject("人等必须在 1~1000 之间");
}
return Promise.resolve();
},
}, },
]} ]}
> >
<InputNumber disabled={groupSizeUnlimit} min='0' max='999' style={{ width: '100%' }} /> <InputNumber disabled={groupAllSize} style={{ width: "100%" }} />
</Form.Item> </Form.Item>
<Checkbox disabled={groupSizeUnlimit} onChange={e => { <Checkbox
disabled={groupAllSize}
onChange={(e) => {
if (e.target.checked) { if (e.target.checked) {
quotationForm.setFieldValue('group_size_max', 1000) quotationForm.setFieldValue("group_size_max", 1000);
setGroupMaxUnlimit(true) setGroupMaxUnlimit(true);
} else { } else {
quotationForm.setFieldValue('group_size_max', 999) setGroupMaxUnlimit(false);
setGroupMaxUnlimit(false)
} }
}}>不限(1000)</Checkbox> }}
>
<span className="font-bold">不限(1000)</span>
</Checkbox>
<Form.Item <Form.Item
label={t('products:group_size')} label={t("products:group_size")}
name='group_size_max' name="group_size_max"
rules={[ rules={[
{ {
required: true, required: true,
message: t('products:Validation.group_size_max'), message: t("products:Validation.group_size_max"),
},
{
validator: (_, value) => {
if (value > 1000 || value < 1) {
return Promise.reject("人等必须在 1~1000 之间");
}
return Promise.resolve();
},
}, },
]} ]}
> >
<InputNumber disabled={groupSizeUnlimit || groupMaxUnlimit} min='0' max='999' style={{ width: '100%' }} /> <InputNumber
disabled={groupAllSize || groupMaxUnlimit}
style={{ width: "100%" }}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('products:use_dates')} label={t("products:use_dates")}
name='use_dates' name="use_dates"
rules={[ rules={[
{ {
required: true, required: true,
message: t('products:Validation.use_dates'), message: t("products:Validation.use_dates"),
}, },
]} ]}
> >
<RangePicker presets={datePresets} style={{ width: '100%' }} /> <RangePicker presets={datePresets} style={{ width: "100%" }} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={t("products:Weekdays")} name="weekdayList">
label={t('products:Weekdays')} <Checkbox.Group options={["5", "6", "7"]} />
name='weekdayList'
>
<Checkbox.Group options={['5', '6', '7']} />
</Form.Item> </Form.Item>
</Modal> </Modal>
</> </>
) );
} };
export default ProductInfoQuotation export default ProductInfoQuotation;

@ -0,0 +1,268 @@
import { useState, useMemo } from 'react';
import { Button, Table, Popover, Typography } from 'antd';
import { useTranslation } from 'react-i18next';
import { HT_HOST } from '@/config';
import { fetchJSON } from '@/utils/request';
import { formatGroupSize } from '@/hooks/useProductsSets';
import { isEmpty, isNotEmpty } from '@/utils/commons';
import { usingStorage } from '@/hooks/usingStorage';
/**
* 产品价格日志
*/
const getPPLogAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_log`, params)
return errcode !== 0 ? [] : result;
};
/**
* 产品价格: 已发布的
*/
const getPPRunningAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_running`, params)
return errcode !== 0 ? [] : result;
};
const parseJson = (str) => {
let result;
if (str === null || str === undefined || str === '') {
return {};
}
try {
result = typeof str === 'string' ? JSON.parse(str) : str;
return Array.isArray(result) ? result.reduce((acc, cur) => ({ ...acc, ...cur }), {}) : result;
} catch (e) {
return {};
}
};
const statesForHideEdited = [1, 2];
export const columnsSets = (t, colorize = true) => [
{
key: 'adult',
title: t('AgeType.Adult'),
width: '12rem',
render: (_, { adult_cost, currency, unit_id, unit_name, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && !statesForHideEdited.includes(audit_state_id);
const ifData = isNotEmpty(_changed.adult_cost) || isNotEmpty(_changed.unit_id) || isNotEmpty(_changed.currency);
const preValue =
ifCompare && ifData ? (
<div className='text-muted line-through '>{`${_changed.adult_cost || adult_cost} ${_changed.currency || currency} / ${t(`PriceUnit.${_changed.unit_id || unit_id}`)}`}</div>
) : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${adult_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}`}</span>
</div>
);
},
},
{
key: 'child',
title: t('AgeType.Child'),
width: '12rem',
render: (_, { child_cost, currency, unit_id, unit_name, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && !statesForHideEdited.includes(audit_state_id);
const ifData = isNotEmpty(_changed.child_cost) || isNotEmpty(_changed.unit_id) || isNotEmpty(_changed.currency);
const preValue =
ifCompare && ifData ? (
<div className='text-muted line-through '>{`${_changed.child_cost || child_cost} ${_changed.currency || currency} / ${t(`PriceUnit.${_changed.unit_id || unit_id}`)}`}</div>
) : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${child_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}`}</span>
</div>
);
},
},
// {key: 'unit', title: t('Unit'), },
{
key: 'groupSize',
dataIndex: ['group_size_min'],
title: t('group_size'),
width: '6rem',
render: (_, { audit_state_id, group_size_min, group_size_max, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const preValue =
colorize && !statesForHideEdited.includes(audit_state_id) && (isNotEmpty(_changed.group_size_min) || isNotEmpty(_changed.group_size_max)) ? (
<div className='text-muted line-through '>{`${_changed.group_size_min ?? group_size_min} - ${_changed.group_size_max ?? group_size_max}`}</div>
) : null;
const editCls = colorize && !statesForHideEdited.includes(audit_state_id) && (isNotEmpty(_changed.group_size_min) || isNotEmpty(_changed.group_size_max)) ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{formatGroupSize(group_size_min, group_size_max)}</span>
</div>
);
},
},
{
key: 'useDates',
dataIndex: ['use_dates_start'],
title: t('use_dates'),
width: '12rem',
render: (_, { use_dates_start, use_dates_end, weekdays, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const preValue =
colorize && !statesForHideEdited.includes(audit_state_id) && (isNotEmpty(_changed.use_dates_start) || isNotEmpty(_changed.use_dates_end)) ? (
<div className='text-muted'>
{isNotEmpty(_changed.use_dates_start) ? <span className=' line-through '>{_changed.use_dates_start}</span> : use_dates_start} ~{' '}
{isNotEmpty(_changed.use_dates_end) ? <span className='t line-through '>{_changed.use_dates_end}</span> : use_dates_end}
</div>
) : null;
const editCls = colorize && !statesForHideEdited.includes(audit_state_id) && (isNotEmpty(_changed.use_dates_start) || isNotEmpty(_changed.use_dates_end)) ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${use_dates_start} ~ ${use_dates_end}`}</span>
</div>
);
},
},
{
key: 'weekdays',
dataIndex: ['weekdays'],
title: t('Weekdays'),
width: '6rem',
render: (text, { weekdays, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && !statesForHideEdited.includes(audit_state_id);
const ifData = !isEmpty(_changed.weekdays);
const _weekdays = ifData
? _changed.weekdays
.split(',')
.filter(Boolean)
.map((w) => t(`common:weekdaysShort.${w}`))
.join(', ')
: '';
const preValue = ifCompare && ifData ? <div className='text-muted line-through '>{_weekdays}</div> : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
const weekdaysTxt = weekdays
.split(',')
.filter(Boolean)
.map((w) => t(`common:weekdaysShort.${w}`))
.join(', ');
return (
<div>
{preValue}
<span className={editCls}>{weekdaysTxt || t('Unlimited')}</span>
</div>
);
},
},
];
const useLogMethod = (method) => {
const { t } = useTranslation('products');
const methodMap = {
'history': {
title: '📑' + t('versionHistory'),
btnText: t('versionHistory'),
fetchData: async (params) => {
const data = await getPPLogAction(params);
return { data };
},
},
'published': {
title: '✅' + t('versionPublished'),
btnText: t('versionPublished'),
fetchData: async (params) => {
const { travel_agency_id, product_id, price_id, use_year } = params;
const data = await getPPRunningAction({ travel_agency_id, product_id_list: product_id, use_year });
return { data: data?.[0]?.quotation || [] };
},
},
};
return methodMap[method];
};
/**
* ProductQuotationLogPopover - A popover component that displays product quotation change logs or published data
*
* This component shows a history of price changes for a specific product quotation in a popover table.
* It supports displaying different data sources (history logs or published data) and shows
* comparison between previous and current values with visual indicators.
*
* @param {Object} props - Component props
* @param {string} props.btnText - The text to display on the trigger button and in the popover header
* @param {'history' | 'published'} props.method - Determines data source - "history" for change logs or "published" for published quotations
* @param {Object} props.triggerProps - Additional props to pass to the trigger button
* @param {number} props.travel_agency_id - ID of the travel agency (used in data fetching)
* @param {number} props.product_id - ID of the product (used in data fetching)
* @param {number} props.price_id - ID of the price entry (used in data fetching)
* @param {number} props.use_year - Year to use for fetching data (used in data fetching)
* @param {Function} props.onOpenChange - Callback function to be called when the popover opens or closes
*/
const ProductQuotationLogPopover = ({ method, triggerProps = {}, onOpenChange, ...props }) => {
const { travel_agency_id, product_id, price_id, use_year } = props;
const { travelAgencyId } = usingStorage();
const { t } = useTranslation('products');
const [open, setOpen] = useState(false);
const [logData, setLogData] = useState([]);
const { title, btnText: methodBtnText, fetchData } = useLogMethod(method);
const tablePagination = useMemo(() => method === 'history' ? { pageSize: 5, position: ['bottomLeft']} : { pageSize: 10, position: ['bottomLeft']}, [method]);
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const { data } = await fetchData({ travel_agency_id: travel_agency_id || travelAgencyId, product_id, price_id, use_year });
setLogData(data);
invokeOpenChange(true);
setLoading(false);
};
const invokeOpenChange = (_open) => {
if (typeof onOpenChange === 'function') {
onOpenChange(_open);
}
};
const columns = [...columnsSets(t, false),
{ title: t('common:time'), dataIndex: 'updatetime', key: 'updatetime', width: '10rem', },
{ title: t('common:operator'), dataIndex: 'update_by', key: 'update_by' }
];
return (
<Popover
placement='bottom'
className=''
rootClassName='w-5/6'
{...props}
title={
<div className='flex justify-between mt-0 gap-4 items-center '>
<Typography.Text strong>{title}</Typography.Text>
<Button
size='small'
onClick={() => {
setOpen(false);
invokeOpenChange(false);
}}>
&times;
</Button>
</div>
}
content={
<>
<Table columns={columns} dataSource={logData} rowKey={'id'} size='small' loading={loading} pagination={tablePagination} />
</>
}
trigger={['click']}
open={open}
onOpenChange={(v) => {
setOpen(v);
invokeOpenChange(v);
}}>
<Button {...triggerProps} onClick={getData} title={title}>
{props.btnText || methodBtnText}
</Button>
</Popover>
);
};
export default ProductQuotationLogPopover;

@ -5,7 +5,7 @@ import { CaretDownOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import useProductsStore from '@/stores/Products/Index'; import useProductsStore from '@/stores/Products/Index';
import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets'; import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets';
import { groupBy, sortBy } from '@/utils/commons'; import { groupBy, isEmpty, sortBy } from '@/utils/commons';
import NewProductModal from './NewProductModal'; import NewProductModal from './NewProductModal';
import ContractRemarksModal from './ContractRemarksModal' import ContractRemarksModal from './ContractRemarksModal'
@ -49,10 +49,11 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
const [activeAgency] = useProductsStore((state) => [state.activeAgency]); const [activeAgency] = useProductsStore((state) => [state.activeAgency]);
const productsTypes = useProductsTypes(); const productsTypes = useProductsTypes();
const [treeData, setTreeData] = useState([]); const [treeData, setTreeData] = useState([]); // render data
const [rawTreeData, setRawTreeData] = useState([]); const [rawTreeData, setRawTreeData] = useState([]);
const [flattenTreeData, setFlattenTreeData] = useState([]); const [flattenTreeData, setFlattenTreeData] = useState([]);
const [expandedKeys, setExpandedKeys] = useState([]); const [expandedKeys, setExpandedKeys] = useState([]);
const [selectedKeys, setSelectedKeys] = useState([]);
const [autoExpandParent, setAutoExpandParent] = useState(true); const [autoExpandParent, setAutoExpandParent] = useState(true);
useEffect(() => { useEffect(() => {
@ -74,28 +75,35 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
const _show = productsTypes const _show = productsTypes
.filter((kk) => hasDataTypes.includes(kk.value)) .filter((kk) => hasDataTypes.includes(kk.value))
.map((ele) => ({ .map((ele) => ({
...ele, ...ele,
title: ele.label, title: ele.label,
key: ele.value, key: ele.value,
children: (agencyProducts[ele.value] || []).map((product) => { children: (agencyProducts[ele.value] || []).reduce((arr, product) => {
const lgc_map = product.lgc_details.reduce((rlgc, clgc) => ({...rlgc, [clgc.lgc]: clgc}), {}); const lgc_map = product.lgc_details.reduce((rlgc, clgc) => ({ ...rlgc, [clgc.lgc]: clgc }), {});
return { // const combindCityList = product.info.city_list.indexOf(city => city.id === product.info.city_id) !== -1 ? product.info.city_list : [...product.info.city_list, { id: product.info.city_id, name: product.info.city_name }];
// title: product.info.title || lgc_map?.['2']?.title || lgc_map?.['1']?.title || '', // const cityListName = product.info.city_list.reduce((acc, city) => {
title: `${product.info.city_name}` + (product.info.title || lgc_map?.['2']?.title || lgc_map?.['1']?.title || product.info.product_title || ''), // return acc.concat([city.name]);
// key: `${ele.value}-${product.info.id}`, // }, []).join(',');
key: product.info.id, const hasCityList = !isEmpty(product.info.city_list) && product.info.city_list.some(cc => cc.id !== product.info.city_id) ? `【含多城市】` : ``;
_raw: product, const combindCityList = [{ id: product.info.city_id, name: product.info.city_name }];
isLeaf: true, const flatCityP = combindCityList.map(city => ({
}}), title: `${city.name}` + (product.info.title || lgc_map?.['2']?.title || lgc_map?.['1']?.title || product.info.product_title || '') + `${hasCityList}`,
// `` // key: `${ele.value}-${product.info.id}`,
// _children: Object.keys(copyAgencyProducts[ele.value] || []).map(city => { key: `${product.info.id}-${city.id}`,
// return { _raw: product,
// title: city, isLeaf: true,
// key: `${ele.value}-${city}`, }));
// children: copyAgencyProducts[ele.value][city], return arr.concat(flatCityP);
// }; }, []),
// }), // ``
})); // _children: Object.keys(copyAgencyProducts[ele.value] || []).map(city => {
// return {
// title: city,
// key: `${ele.value}-${city}`,
// children: copyAgencyProducts[ele.value][city],
// };
// }),
}));
setTreeData(_show); setTreeData(_show);
setRawTreeData(_show); setRawTreeData(_show);
setFlattenTreeData(flattenTreeFun(_show)); setFlattenTreeData(flattenTreeFun(_show));
@ -106,9 +114,27 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
return () => {}; return () => {};
}, [productsTypes, agencyProducts]); }, [productsTypes, agencyProducts]);
useEffect(() => {
if (isEmpty(editingProduct)) {
return () => {};
}
const allKeysWithCity = [...editingProduct.info.city_list, { id: editingProduct.info.city_id, name: editingProduct.info.city_name }].map(
(city) => `${editingProduct.info.id}-${city.id}`
);
setSelectedKeys(allKeysWithCity);
return () => {};
}, [editingProduct?.info?.id]);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const onSearch = ({ target: { value } }) => { const onSearch = ({ target: { value } }) => {
// const { value } = e.target; // const { value } = e.target;
if (isEmpty(value)) {
setTreeData(rawTreeData);
setSearchValue(value);
return;
}
const newExpandedKeys = flattenTreeData const newExpandedKeys = flattenTreeData
.filter((item) => item.title.includes(value)) .filter((item) => item.title.includes(value))
.map((item) => getParentKey(item.key, rawTreeData)) .map((item) => getParentKey(item.key, rawTreeData))
@ -116,10 +142,17 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
setExpandedKeys(newExpandedKeys); setExpandedKeys(newExpandedKeys);
setSearchValue(value); setSearchValue(value);
setAutoExpandParent(true); setAutoExpandParent(true);
const matchTree = rawTreeData.map(node1 => {
const _find = node1.children.filter(node2 => node2.title.includes(value));
return _find.length > 0 ? {...node1, children: _find} : null;
}).filter(node => node);
setTreeData(matchTree);
}; };
const handleNodeSelect = (selectedKeys, { node }) => { const handleNodeSelect = (selectedKeys, { node }) => {
if (node._raw) { if (node._raw) {
setEditingProduct(node._raw); setEditingProduct(node._raw);
const allKeysWithCity = [...node._raw.info.city_list, { id: node._raw.info.city_id, name: node._raw.info.city_name }].map((city) => `${node._raw.info.id}-${city.id}`);
setSelectedKeys(allKeysWithCity);
} else { } else {
// : / // : /
// const isExpand = expandedKeys.includes(selectedKeys[0]); // const isExpand = expandedKeys.includes(selectedKeys[0]);
@ -164,7 +197,7 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
<Tree <Tree
blockNode blockNode
showLine defaultExpandAll expandAction={'doubleClick'} showLine defaultExpandAll expandAction={'doubleClick'}
selectedKeys={[editingProduct?.info?.id || editingProduct?.info?.product_type_id]} selectedKeys={selectedKeys} multiple
switcherIcon={<CaretDownOutlined />} switcherIcon={<CaretDownOutlined />}
onSelect={handleNodeSelect} onSelect={handleNodeSelect}
treeData={treeData} treeData={treeData}

@ -0,0 +1,45 @@
import { useNavigate } from "react-router-dom";
import { Row, DatePicker, Flex, Col, Typography } from "antd";
import dayjs from "dayjs";
import { usingStorage } from "@/hooks/usingStorage";
function PickYear() {
const navigate = useNavigate();
const { travelAgencyId } = usingStorage();
return (
<>
<Row justify="center">
<Col span={4}>
<Flex gap="middle" vertical>
<Typography.Title className="text-center" level={3}>
请选择产品年份
</Typography.Title>
<DatePicker
className="w-full"
size="large"
variant="underlined"
needConfirm
inputReadOnly={true}
minDate={dayjs().add(-30, "year")}
maxDate={dayjs().add(2, "year")}
allowClear={false}
picker="year"
styles={{
root: {
color: 'red'
}
}}
open={true}
onOk={(date) => {
const useYear = date.year();
navigate(`/products/${travelAgencyId}/${useYear}/all/edit`);
}}
/>
</Flex>
</Col>
</Row>
</>
);
}
export default PickYear;
Loading…
Cancel
Save