diff --git a/public/locales/en/products.json b/public/locales/en/products.json index b468364..e5e841c 100644 --- a/public/locales/en/products.json +++ b/public/locales/en/products.json @@ -48,7 +48,7 @@ "OpenHours": "Open Hours", "Duration": "Duration", "KM": "KM", - "RecommendsRate": "RecommendsRate", + "RecommendsRate": "Recommends Rate", "OpenWeekdays": "Open Weekdays", "DisplayToC": "Display To C", "Dept": "Dept", @@ -116,20 +116,64 @@ "Weekdays": "Weekdays" }, + "sureSubmitAudit": "确认提交所有产品审核? 提交后,所有产品将进入待审核流程. ", "FormTooltip": { "Type": "Product Type", "Title": "Title", "Code": "Code", - "City": "City", + "City": "起始城市,举例:北京", "Dept": "Department", - "Duration": "Duration", - "RecommendsRate": "RecommendsRate", + "Duration": "", + "RecommendsRate": "Recommends Rate", "OpenHours": "Open Hours", "OpenWeekdays": "Open Weekdays", "DisplayToC": "Display Type", - "KM": "KM", + "KM": "往返", "Description": "Description", - "Remarks": "Memo" + "Remarks": "", + "NewTitle": { + "6": "", + "B": "A点-B点,举例:桂林-龙胜", + "J": "A点-B点时长车费,举例:张家界5晚6天车费", + "Q": "城市语种导游工资单位,举例:北京英文导游工资(元/天/团)", + "7": "官方景点名称,举例:陕西历史博物馆", + "R": "普通、豪华、特色餐标,举例:普通餐标", + "8": "举例:故宫导游门票", + "D": "城市A点-B点时间包含内容,举例:北京市区一日游车导" + }, + "----Todo: 下面一组待定": "#", + "6": { + "Title": "", + "#": "" + }, + "B": { + "Title": "A点-B点,举例:桂林-龙胜", + "#": "" + }, + "J": { + "Title": "A点-B点时长车费,举例:张家界5晚6天车费", + "#": "" + }, + "Q": { + "Title": "城市语种导游工资单位,举例:北京英文导游工资(元/天/团)", + "#": "" + }, + "7": { + "Title": "官方景点名称,举例:陕西历史博物馆", + "#": "" + }, + "R": { + "Title": "普通、豪华、特色餐标,举例:普通餐标", + "#": "" + }, + "8": { + "Title": "举例:故宫导游门票", + "#": "" + }, + "D": { + "Title": "城市A点-B点时间包含内容,举例:北京市区一日游车导", + "#": "" + } }, "LgcModal": { diff --git a/public/locales/zh/products.json b/public/locales/zh/products.json index 1778f9d..37cfb4f 100644 --- a/public/locales/zh/products.json +++ b/public/locales/zh/products.json @@ -104,20 +104,32 @@ "Weekdays": "周末", "Operation": "Operation" }, - "FormTooltop": { + + "sureSubmitAudit": "确认提交所有产品审核? 提交后,所有产品将进入待审核流程. ", + "FormTooltip": { "Type": "Product Type", "Title": "Title", "Code": "Code", - "City": "City", - "dept": "Department", - "Duration": "Duration", - "RecommendsRate": "RecommendsRate", + "City": "起始城市,举例:北京", + "Dept": "Department", + "Duration": "", + "RecommendsRate": "Recommends Rate", "OpenHours": "Open Hours", "OpenWeekdays": "Open Weekdays", "DisplayToC": "Display Type", - "KM": "KM", + "KM": "往返", "Description": "Description", - "Remarks": "Remarks" + "Remarks": "", + "NewTitle": { + "6": "", + "B": "A点-B点,举例:桂林-龙胜", + "J": "A点-B点时长车费,举例:张家界5晚6天车费", + "Q": "城市语种导游工资单位,举例:北京英文导游工资(元/天/团)", + "7": "官方景点名称,举例:陕西历史博物馆", + "R": "普通、豪华、特色餐标,举例:普通餐标", + "8": "举例:故宫导游门票", + "D": "城市A点-B点时间包含内容,举例:北京市区一日游车导" + } }, "LgcModal": { diff --git a/src/hooks/useProductsSets.js b/src/hooks/useProductsSets.js index b4e9abb..1900465 100644 --- a/src/hooks/useProductsSets.js +++ b/src/hooks/useProductsSets.js @@ -145,7 +145,7 @@ export const useNewProductRecord = () => { 'dept_id': 0, 'dept_name': '', 'display_to_c': 0, - 'km': 0, + 'km': undefined, 'city_id': 0, 'city_name': '', 'open_hours': '', diff --git a/src/views/App.jsx b/src/views/App.jsx index 4689721..fa8a61f 100644 --- a/src/views/App.jsx +++ b/src/views/App.jsx @@ -15,6 +15,8 @@ import useNoticeStore from '@/stores/Notice'; import useAuthStore from '@/stores/Auth'; import { useThemeContext } from '@/stores/ThemeContext'; import { usingStorage } from '@/hooks/usingStorage'; +import { useDefaultLgc } from '@/i18n/LanguageSwitcher'; +import { appendRequestParams } from '@/utils/request' import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT } from '@/config'; @@ -63,9 +65,11 @@ function App() { defaultPath = splitPath[1] } + const { language } = useDefaultLgc(); const [antdLng, setAntdLng] = useState(enLocale); useEffect(() => { setAntdLng(i18n.language === 'en' ? enLocale : zhLocale); + appendRequestParams('lgc', language); }, [i18n.language]) return ( diff --git a/src/views/products/Detail.jsx b/src/views/products/Detail.jsx index 6f55b5c..5967b20 100644 --- a/src/views/products/Detail.jsx +++ b/src/views/products/Detail.jsx @@ -60,8 +60,6 @@ function Detail() { - - setAddProductVisible(false)} onCancel={() => setAddProductVisible(false)} /> )} diff --git a/src/views/products/Detail/Header.jsx b/src/views/products/Detail/Header.jsx index 84d7bb0..93beec6 100644 --- a/src/views/products/Detail/Header.jsx +++ b/src/views/products/Detail/Header.jsx @@ -12,6 +12,7 @@ import { PERM_PRODUCTS_OFFER_AUDIT, PERM_PRODUCTS_OFFER_PUT } from '@/config'; import dayjs from 'dayjs'; import VendorSelector from '@/components/VendorSelector'; import AuditStateSelector from '@/components/AuditStateSelector'; +import NewProductModal from './NewProductModal'; const Header = ({ refresh, newActionable, ...props }) => { const location = useLocation(); @@ -174,17 +175,12 @@ const Header = ({ refresh, newActionable, ...props }) => { {/* 编辑 */} {isEditPage && ( - - - + )} - {activeAgencyState === 0 && ( + {/* {activeAgencyState === 0 && ( */} <> - + + + { - onCancel(); + // onCancel(); + setOpen(false); formInstance?.resetFields(); }} destroyOnClose @@ -105,8 +122,6 @@ export const NewProductModal = ({ source, action = '#' | 'o', open, onSubmit, on } }}> { setFormInstance(instance); diff --git a/src/views/products/Detail/PriceCompactInput.jsx b/src/views/products/Detail/PriceCompactInput.jsx index c2b860e..2485b52 100644 --- a/src/views/products/Detail/PriceCompactInput.jsx +++ b/src/views/products/Detail/PriceCompactInput.jsx @@ -106,4 +106,8 @@ const PriceCompactInput = (props) => { ) } -export default PriceCompactInput \ No newline at end of file +<<<<<<< HEAD +export default PriceCompactInput +======= +export default PriceCompactInput +>>>>>>> acaf5a3de7b7197af62517e06d7fdbb7a4a48ace diff --git a/src/views/products/Detail/ProductInfo.jsx b/src/views/products/Detail/ProductInfo.jsx index 87f148e..95e759a 100644 --- a/src/views/products/Detail/ProductInfo.jsx +++ b/src/views/products/Detail/ProductInfo.jsx @@ -9,6 +9,7 @@ import { isEmpty, pick } from '@/utils/commons'; import ProductInfoForm from './ProductInfoForm'; import { usingStorage } from '@/hooks/usingStorage'; import Extras from './Extras'; +import NewProductModal from './NewProductModal'; const ProductInfo = ({ ...props }) => { const { t } = useTranslation(); @@ -22,18 +23,25 @@ const ProductInfo = ({ ...props }) => { const [activeAgency, editingProduct, setEditingProduct] = useProductsStore((state) => [state.activeAgency, state.editingProduct, state.setEditingProduct]); const [extrasVisible, setExtrasVisible] = useState(false); - const [editable, setEditable] = useState(false); + const [editablePerm, setEditablePerm] = useState(false); + const [infoEditable, setInfoEditable] = useState(false); + const [priceEditable, setPriceEditable] = useState(false); const topPerm = isPermitted(PERM_PRODUCTS_MANAGEMENT); // 高级权限 useEffect(() => { const hasHT = (editingProduct?.info?.htid || 0) > 0; // const hasAuditPer = isPermitted(PERM_PRODUCTS_OFFER_AUDIT); const hasEditPer = isPermitted(PERM_PRODUCTS_OFFER_PUT); - setEditable(topPerm || (!hasHT && hasEditPer)); + + setEditablePerm(topPerm || hasEditPer); // setEditable(topPerm || (hasAuditPer ? true : (!hasHT && hasEditPer))); // setEditable(true); // debug: 0 // console.log('editable', hasAuditPer, (notAudit && hasEditPer)); + setInfoEditable(topPerm || (!hasHT && hasEditPer)); + + const _priceEditable = [-1, 3].includes(activeAgency?.audit_state_id) || isEmpty(editingProduct?.info?.id); + setPriceEditable(topPerm || (_priceEditable && hasEditPer)); - const showExtras = topPerm && !isEmpty(editingProduct) && hasHT; + const showExtras = topPerm && hasHT; // !isEmpty(editingProduct) && setExtrasVisible(showExtras); return () => {}; }, [activeAgency, editingProduct]); @@ -42,18 +50,19 @@ const ProductInfo = ({ ...props }) => { values.travel_agency_id = activeAgency.travel_agency_id; const copyNewProduct = structuredClone(newProductRecord); const poster = { - 'audit_state': '-1', + ...(topPerm ? {} : { 'audit_state': -1 }), // 高级权限: 不变更状态值 // "create_date": dayjs().format('YYYY-MM-DD HH:mm:ss'), // "created_by": userId, 'travel_agency_id': activeAgency.travel_agency_id, // "travel_agency_name": "", // "lastedit_changed": "", }; - /** lgc_details */ const copyFields = pick(editingProduct.info, ['product_type_id']); // 'title', const readyToSubInfo = { ...copyNewProduct.info, ...editingProduct.info, ...values.info, ...copyFields, type: copyFields.product_type_id, ...poster }; + readyToSubInfo.dept = Number(readyToSubInfo.dept); // console.log('onSave', editingProduct.info, readyToSubInfo); - const prevLgcDetailsMapped = editingProduct.lgc_details.reduce((r, c) => ({ ...r, [c.lgc]: { ...c, description: c.descriptions } }), {}); // todo: description字段不一致 + /** lgc_details */ + const prevLgcDetailsMapped = editingProduct.lgc_details.reduce((r, c) => ({ ...r, [c.lgc]: { ...c, description: c.descriptions } }), {}); const mergedLgc = { ...prevLgcDetailsMapped, ...values.lgc_details_mapped }; // console.log(values); @@ -64,15 +73,15 @@ const ProductInfo = ({ ...props }) => { travel_agency_id: activeAgency.travel_agency_id, info: readyToSubInfo, lgc_details: Object.values(mergedLgc), - quotation: values.quotation, // || editingProduct.quotation, // 没改动, 就用原来的 - }).catch(ex => { + quotation: values.quotation.map((q) => ({ ...q, unit: Number(q.unit || q.unit_id), unit_id: Number(q.unit_id) })), // || editingProduct.quotation, // 没改动, 就用原来的 + }).catch((ex) => { setLoading(false); notification.error({ message: 'Notification', description: ex.message, placement: 'top', duration: 4, - }) + }); }); setLoading(false); success ? message.success(t('Success')) : message.error(t('Failed')); @@ -90,10 +99,18 @@ const ProductInfo = ({ ...props }) => { ]} /> -

{t('products:EditComponents.info')}

- - - {extrasVisible && } + {isEmpty(editingProduct) ? ( +
+ +
+ ) : ( + <> +

{t('products:EditComponents.info')}

+ + + {extrasVisible && } + + )} ); }; diff --git a/src/views/products/Detail/ProductInfoForm.jsx b/src/views/products/Detail/ProductInfoForm.jsx index c0fefe6..f7dff06 100644 --- a/src/views/products/Detail/ProductInfoForm.jsx +++ b/src/views/products/Detail/ProductInfoForm.jsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Form, Input, Row, Col, Select, Button, InputNumber, Checkbox } from 'antd'; +import { App, Form, Input, Row, Col, Select, Button, InputNumber, Checkbox } from 'antd'; import { objectMapper, isEmpty, isNotEmpty } from '@/utils/commons'; import { useTranslation } from 'react-i18next'; import { useWeekdays } from '@/hooks/useDatePresets'; @@ -11,7 +11,8 @@ import ProductInfoLgc from './ProductInfoLgc'; import ProductInfoQuotation from './ProductInfoQuotation'; import { useHTLanguageSetsMapVal } from '@/hooks/useHTLanguageSets'; -const InfoForm = ({ onSubmit, onReset, onValuesChange, editable: _editable, showSubmit, confirmText, formName, ...props }) => { +const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditable, priceEditable, showSubmit, confirmText, formName, ...props }) => { + const { notification } = App.useApp(); const { t } = useTranslation('products'); const HTLanguageSetsMapVal = useHTLanguageSetsMapVal(); const [loading, editingProduct] = useProductsStore((state) => [state.loading, state.editingProduct]); @@ -28,19 +29,24 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editable: _editable, show const filedsets = useProductsTypesFieldsets(editingProduct?.info?.product_type_id); const shows = filedsets[0]; - const [editable, setEditable] = useState(true); + // const [editable, setEditable] = useState(true); + const [formEditable, setFormEditable] = useState(true); + const [showSave, setShowSave] = useState(true); useEffect(() => { form.resetFields(); - form.setFieldValue('city', { value: editingProduct?.info?.city_id, label: editingProduct?.info?.city_name }); + 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 }); 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); - const editable0 = isEmpty(editingProduct) ? false : _editable; // 空对象未操作 - setEditable(editable0); + setFormEditable(infoEditable || priceEditable); + + // const editable0 = isEmpty(editingProduct) ? false : editablePerm; // 空对象未操作 + setShowSave(infoEditable || priceEditable); + // setEditable(editable0); return () => {}; - }, [editingProduct?.info?.id, _editable]); + }, [editingProduct?.info?.id, editablePerm, infoEditable, priceEditable]); const onFinish = (values) => { console.log('Received values of form, origin form value: \n', values); @@ -53,6 +59,12 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editable: _editable, show const onFinishFailed = ({ values, errorFields }) => { console.log('form validate failed', '\nform values:', values, '\nerrorFields', errorFields); + notification.warning({ + message: '数据未填写完整', + // description: '数据未填写完整', + placement: 'top', + duration: 4, + }) }; const handleReset = () => { @@ -77,16 +89,16 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editable: _editable, show <>
+ onFinishFailed={onFinishFailed} scrollToFirstError > - {getFields({ sort, initialValue: editingProduct?.info, hides, shows, fieldProps, fieldComProps, form, t, dataSets: { weekdays }, editable })} + {getFields({ sort, initialValue: editingProduct?.info, hides, shows, fieldProps, fieldComProps, form, t, dataSets: { weekdays }, editable: infoEditable })} {/* {showSubmit && ( @@ -118,11 +130,11 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editable: _editable, show }, }), ]}> - + - +