| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- import { DropdownDirective } from '../../shared/dropdown.directive';
- import { CommonModule } from '@angular/common';
- import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';
- import { FormsModule } from '@angular/forms';
- import { ActivatedRoute } from '@angular/router';
- import { AlertCircle, ImagePlus, LucideAngularModule, Save, Send, X } from 'lucide-angular';
- import { finalize, firstValueFrom } from 'rxjs';
- import { ApiError, ApiService } from '../../core/api.service';
- import { buildFormSubmission, FormValue } from '../../core/form-payload';
- import { FormField, PageMeta } from '../../core/models';
- import { PlatformService } from '../../core/platform.service';
- import { SessionService } from '../../core/session.service';
- @Component({
- selector: 'app-form-page',
- standalone: true,
- imports: [DropdownDirective, CommonModule, FormsModule, LucideAngularModule],
- templateUrl: './form-page.component.html',
- styleUrl: './form-page.component.scss',
- changeDetection: ChangeDetectionStrategy.OnPush,
- })
- export class FormPageComponent implements OnInit {
- readonly meta = inject(ActivatedRoute).snapshot.data['meta'] as PageMeta;
- readonly loading = signal(false);
- readonly uploadProgress = signal<Record<string, boolean>>({});
- readonly nodeOptions = signal<Array<{ label: string; value: string | number }>>([]);
- readonly productOptions = signal<Array<{ label: string; value: string | number }>>([]);
- readonly appealAudit = signal<Record<string, unknown>>({});
- readonly loadError = signal('');
- readonly icons = { AlertCircle, ImagePlus, Save, Send, X };
- values: Record<string, FormValue> = {};
- private loadedRecord: Record<string, unknown> = {};
- private loadedAddon: unknown;
- private readonly route = inject(ActivatedRoute);
- private readonly api = inject(ApiService);
- private readonly session = inject(SessionService);
- private readonly platform = inject(PlatformService);
- ngOnInit(): void {
- for (const field of this.meta.fields ?? []) {
- this.values[field.key] = field.type === 'checkbox' ? false : field.options?.[0]?.value ?? '';
- }
- if (this.meta.path === 'pages/shop/order_appeal') this.values['concat'] = this.session.user()?.mobile ?? '';
- if (this.meta.path === 'pages/member/myshop_add') this.loadProductNodes();
- if (this.meta.api) this.loadWhenApplicable();
- }
- private loadWhenApplicable(): void {
- const action = this.meta.api?.action;
- const id = this.route.snapshot.queryParamMap.get('id');
- if (!action) return;
- if (['receaddr_get', 'invoice_get', 'user_bank_get', 'product_get'].includes(action) && !id) return;
- if (action === 'order_get' && !id) {
- this.loadError.set('缺少订单编号,请从订单页面进入');
- return;
- }
- this.load();
- }
- load(): void {
- const binding = this.meta.api;
- if (!binding) return;
- this.loadError.set('');
- const payload = { ...binding.params, ...this.route.snapshot.queryParams, uid: this.session.user()?.userId };
- this.loading.set(true);
- const request = binding.method === 'post' ? this.api.post<Record<string, unknown>>(binding.action, payload) : this.api.get<Record<string, unknown>>(binding.action, payload);
- request.pipe(finalize(() => this.loading.set(false))).subscribe({
- next: ({ result, addon }) => {
- const record = this.asRecord(Array.isArray(result) ? result[0] : result);
- this.loadedRecord = record;
- this.loadedAddon = addon;
- if (this.isOrderForm()) this.prepareOrderProducts(addon);
- for (const field of this.meta.fields ?? []) {
- const value = this.loadedValue(field.key, record, addon);
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') this.values[field.key] = value;
- }
- if (this.meta.path === 'pages/shop/order_appeal') this.loadExistingAppeal(record);
- },
- error: (error: ApiError) => this.loadError.set(error.message),
- });
- }
- async chooseFile(field: FormField, event: Event): Promise<void> {
- const input = event.target as HTMLInputElement;
- const files = Array.from(input.files ?? []);
- input.value = '';
- if (!files.length) return;
- const current = this.fileUrls(field);
- const available = Math.max(0, this.fileLimit(field) - current.length);
- const selected = files.slice(0, this.isMultiFile(field) ? available : 1);
- if (!selected.length) {
- this.platform.toast(`最多上传${this.fileLimit(field)}张图片`, 'error');
- return;
- }
- this.uploadProgress.update((value) => ({ ...value, [field.key]: true }));
- try {
- const uploaded: string[] = [];
- for (const file of selected) {
- const { result } = await firstValueFrom(this.api.upload(file));
- uploaded.push(String(result));
- }
- if (this.meta.path === 'pages/member/myshop_add_procon' && field.key === 'image') {
- const html = uploaded.map((url) => `<p><img src="${url}" /></p>`).join('');
- this.values['Procontent'] = `${String(this.values['Procontent'] ?? '')}${html}`;
- this.values[field.key] = '';
- } else if (this.isMultiFile(field)) {
- this.values[field.key] = JSON.stringify([...current, ...uploaded].map((url) => ({ url, name: '', desc: '', link: '' })));
- } else {
- this.values[field.key] = uploaded[0];
- }
- this.platform.toast('上传成功', 'success');
- } catch (error) {
- this.platform.toast(error instanceof ApiError ? error.message : '上传失败', 'error');
- } finally {
- this.uploadProgress.update((value) => ({ ...value, [field.key]: false }));
- }
- }
- fileUrls(field: FormField): string[] {
- const raw = String(this.values[field.key] ?? '').trim();
- if (!raw) return [];
- try {
- const parsed = JSON.parse(raw) as Array<{ url?: string }>;
- if (Array.isArray(parsed)) return parsed.map((item) => String(item?.url ?? '')).filter(Boolean);
- } catch { /* Single paths and legacy pipe-separated image values are valid. */ }
- return raw.split('|').map((url) => url.trim()).filter(Boolean);
- }
- removeFile(field: FormField, index: number): void {
- const urls = this.fileUrls(field).filter((_, itemIndex) => itemIndex !== index);
- this.values[field.key] = this.isMultiFile(field)
- ? JSON.stringify(urls.map((url) => ({ url, name: '', desc: '', link: '' })))
- : '';
- }
- imageUrl(path: string): string {
- return this.api.assetUrl(path);
- }
- isMultiFile(field: FormField): boolean {
- return ['images', 'pics', 'license'].includes(field.key);
- }
- fileLimit(field: FormField): number {
- if (field.key === 'images') return 5;
- if (field.key === 'pics') return 20;
- if (field.key === 'license') return 20;
- return 1;
- }
- visibleField(field: FormField): boolean {
- if (this.meta.path === 'pages/member/invoice_add' && ['UserMobile', 'UserAddress', 'UserBankName', 'UserBankAccount'].includes(field.key)) {
- return Number(this.values['InvType']) === 1;
- }
- if (this.meta.path === 'pages/member/group_join') {
- const mode = String(this.values['mode'] ?? 'supplier');
- if (['name', 'bankacc', 'license', 'remark'].includes(field.key)) return mode === 'supplier';
- if (['mobile', 'call', 'tips'].includes(field.key)) return mode === 'invite';
- }
- if (this.meta.path === 'pages/member/myshop_add' && field.key === 'Stock' && this.loadedRecord['ID']) return false;
- return true;
- }
- fieldOptions(field: FormField): Array<{ label: string; value: string | number }> {
- if (this.meta.path === 'pages/member/myshop_add' && field.key === 'Nodeid') return this.nodeOptions();
- if (this.meta.path === 'pages/shop/order_comment_add' && field.key === 'proId') return this.productOptions();
- return field.options ?? [];
- }
- fieldDisabled(field: FormField): boolean {
- if (this.appealAudit()['ID']) return true;
- return this.meta.path === 'pages/member/myshop_add' && field.key === 'Nodeid' && Boolean(this.loadedRecord['ID']);
- }
- isOrderForm(): boolean { return ['pages/shop/order_appeal', 'pages/shop/order_comment_add'].includes(this.meta.path); }
- orderNumber(): string { return String(this.loadedRecord['OrderNo'] ?? '-'); }
- orderProducts(): Array<Record<string, unknown>> {
- const addon = this.asRecord(this.loadedAddon);
- return Array.isArray(addon['cart']) ? addon['cart'] as Array<Record<string, unknown>> : [];
- }
- productName(item: Record<string, unknown>): string { return String(item['Proname'] ?? this.parseObject(item['ProInfo'])['Proname'] ?? '商品'); }
- productImage(item: Record<string, unknown>): string { return this.imageUrl(String(item['proimg'] ?? this.parseObject(item['ProInfo'])['Thumbnails'] ?? '')); }
- auditText(): string {
- const audit = this.appealAudit();
- if (!audit['ID']) return '';
- return Number(audit['Pubstart'] ?? 0) === 0 ? '处理中:平台正在处理,请耐心等待' : `已处理:${audit['PubRemind'] || '暂无处理说明'}`;
- }
- fieldRequired(field: FormField): boolean {
- if (this.meta.path === 'pages/member/invoice_add' && field.key === 'UserMobile') return Number(this.values['InvType']) === 1;
- if (this.meta.path === 'pages/member/group_join') {
- const mode = String(this.values['mode'] ?? 'supplier');
- return mode === 'supplier'
- ? ['name', 'bankacc', 'license'].includes(field.key)
- : field.key === 'mobile';
- }
- return Boolean(field.required);
- }
- fieldLabel(field: FormField): string {
- if (this.meta.path !== 'pages/member/invoice_add') return field.label;
- const company = Number(this.values['InvType']) === 1;
- const labels: Record<string, string> = company
- ? { InvHead: '企业名称', UserCode: '统一信用代码', UserEmail: '企业注册邮箱' }
- : { InvHead: '姓名', UserCode: '身份证号码', UserEmail: '联系邮箱' };
- return labels[field.key] ?? field.label;
- }
- statusText(): string {
- if (this.meta.path !== 'pages/member/rnauth') return '';
- if (!Number(this.loadedRecord['ID'] ?? 0)) return '未提交实名信息';
- return Number(this.loadedRecord['Status'] ?? 0) === 99 ? '已实名认证' : '正在审核';
- }
- sendCode(): void {
- const mobile = String(this.values['mobile'] ?? this.values['MobileNum'] ?? this.session.user()?.mobile ?? '').trim();
- if (!/^1\d{10}$/.test(mobile)) {
- this.platform.toast('请先填写正确的手机号码', 'error');
- return;
- }
- this.platform.loading.set(true);
- const source = this.meta.path.endsWith('passwd2') ? 'reset_paypwd' : this.meta.path.endsWith('passwd') ? 'reset_loginpwd' : 'update';
- this.api.post('mcode_send', { mobile, va: 1, source, uid: this.session.user()?.userId }).pipe(finalize(() => this.platform.loading.set(false))).subscribe({
- next: () => this.platform.toast('验证码已发送', 'success'),
- error: (error: ApiError) => this.platform.toast(error.message, 'error'),
- });
- }
- async submit(): Promise<void> {
- if (this.loadError()) {
- this.platform.toast('当前页面数据未正确加载,无法保存', 'error');
- return;
- }
- if (this.appealAudit()['ID']) { this.platform.toast('该订单已有退换货申请', 'info'); return; }
- for (const field of this.meta.fields ?? []) {
- if (!this.visibleField(field)) continue;
- if (this.fieldRequired(field) && !String(this.values[field.key] ?? '').trim()) {
- this.platform.toast(`${field.type === 'file' ? '请上传' : '请填写'}${this.fieldLabel(field)}`, 'error');
- return;
- }
- const value = String(this.values[field.key] ?? '').trim();
- if (field.type === 'tel' && value && !/^1\d{10}$/.test(value)) {
- this.platform.toast(`请填写正确的${field.label}`, 'error');
- return;
- }
- if (field.type === 'email' && value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
- this.platform.toast(`请填写正确的${field.label}`, 'error');
- return;
- }
- }
- if (!this.validatePairs()) return;
- const user = this.session.user();
- if (this.meta.path === 'pages/member/myshop_add' && !this.loadedRecord['ID']) {
- if (Number(user?.point ?? 0) < 1000) {
- this.platform.toast('您的弘法令不足,发布新商品需要1000弘法令', 'error');
- return;
- }
- if (!await this.platform.confirm('发布新商品需要1000弘法令,确定继续?')) return;
- }
- if (this.meta.path === 'pages/member/rnauth' && this.loadedRecord['ID']) {
- if (!await this.platform.confirm('确定放弃当前实名信息并重新提交?')) return;
- }
- const submission = buildFormSubmission(this.meta, this.values, {
- uid: user?.userId,
- id: this.route.snapshot.queryParamMap.get('id') ?? undefined,
- user,
- loaded: this.loadedRecord,
- addon: this.loadedAddon,
- });
- if (!submission) return;
- this.platform.loading.set(true);
- this.api.post(submission.action, submission.payload).pipe(finalize(() => this.platform.loading.set(false))).subscribe({
- next: () => {
- this.platform.toast('保存成功', 'success');
- if (this.meta.path === 'pages/member/settings') {
- this.session.patchUser(this.values as Partial<import('../../core/models').AppUser>);
- return;
- }
- if (this.meta.path === 'pages/member/rnauth') {
- this.load();
- return;
- }
- setTimeout(() => history.back(), 450);
- },
- error: (error: ApiError) => this.platform.toast(error.message, 'error'),
- });
- }
- private validatePairs(): boolean {
- const pairs: Array<[string, string, string]> = [
- ['passwd', 'passwd2', '两次输入的登录密码不一致'],
- ['paypwd', 'paypwd2', '两次输入的支付密码不一致'],
- ];
- for (const [first, second, message] of pairs) {
- if (first in this.values && second in this.values && this.values[first] !== this.values[second]) {
- this.platform.toast(message, 'error');
- return false;
- }
- }
- if (this.meta.path === 'pages/member/receaddr_add') {
- const zipcode = String(this.values['Zipcode'] ?? '');
- if (zipcode && !/^\d{6}$/.test(zipcode)) {
- this.platform.toast('请填写正确的邮政编码', 'error');
- return false;
- }
- }
- if (this.meta.path === 'pages/member/wxauth_after') {
- const password = String(this.values['passwd'] ?? '');
- const payPassword = String(this.values['paypwd'] ?? '');
- if (password.length < 6 || password.length > 18) {
- this.platform.toast('登录密码应为6至18位', 'error');
- return false;
- }
- if (!/^\d{6}$/.test(payPassword)) {
- this.platform.toast('支付密码必须为6位数字', 'error');
- return false;
- }
- }
- if (['pages/member/passwd', 'pages/member/passwd2'].includes(this.meta.path)) {
- const isPay = this.meta.path.endsWith('passwd2');
- const password = String(this.values[isPay ? 'paypwd' : 'passwd'] ?? '');
- if (isPay ? !/^\d{6}$/.test(password) : password.length < 6 || password.length > 18) {
- this.platform.toast(isPay ? '支付密码必须为6位数字' : '登录密码应为6至18位', 'error');
- return false;
- }
- }
- if (this.meta.path === 'pages/member/invoice_add') {
- const company = Number(this.values['InvType']) === 1;
- const code = String(this.values['UserCode'] ?? '').trim();
- if (!company && !this.isIdCard(code)) {
- this.platform.toast('请输入正确的身份证号码', 'error');
- return false;
- }
- if (company && !/^1\d{10}$/.test(String(this.values['UserMobile'] ?? ''))) {
- this.platform.toast('请输入正确的企业注册电话', 'error');
- return false;
- }
- }
- if (this.meta.path === 'pages/member/rnauth') {
- const idCard = String(this.values['idCard'] ?? '').trim();
- if (!this.isIdCard(idCard)) {
- this.platform.toast('请填写正确的身份证件号', 'error');
- return false;
- }
- const age = new Date().getFullYear() - Number(idCard.slice(6, 10));
- if (age < 18 || age > 65) {
- this.platform.toast(age < 18 ? '最小注册年龄为18岁' : '最大注册年龄为65岁', 'error');
- return false;
- }
- }
- if (this.meta.path === 'pages/shop/order_comment_add') {
- const score = Number(this.values['score'] ?? 0);
- if (score < 1 || score > 5) {
- this.platform.toast('评分应为1至5分', 'error');
- return false;
- }
- }
- if (this.meta.path === 'pages/member/myshop_add') {
- if (Number(this.values['LinPrice']) <= 0 || Number(this.values['ShiPrice']) <= 0) {
- this.platform.toast('商品价格必须大于0', 'error');
- return false;
- }
- if (!this.loadedRecord['ID'] && (!Number.isInteger(Number(this.values['Stock'])) || Number(this.values['Stock']) <= 0)) {
- this.platform.toast('初始库存必须是大于0的整数', 'error');
- return false;
- }
- }
- if (this.meta.path === 'pages/member/group_join') {
- const addon = this.asRecord(this.loadedAddon);
- if (Number(addon['vip'] ?? 0) < 3) {
- this.platform.toast('您必须成为初级合伙人后才可进行操作', 'error');
- return false;
- }
- }
- return true;
- }
- private isIdCard(value: string): boolean {
- if (!/^\d{17}[\dXx]$/.test(value)) return false;
- const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
- const checks = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
- const sum = weights.reduce((total, weight, index) => total + Number(value[index]) * weight, 0);
- return checks[sum % 11] === value[17].toUpperCase();
- }
- private loadedValue(key: string, record: Record<string, unknown>, addon: unknown): unknown {
- if (key in record) return record[key];
- const aliases: Record<string, string> = {
- Nodeid: 'NodeID',
- realName: 'RealName',
- idCard: 'IdCard',
- mobile: 'Mobile',
- address: 'Address',
- };
- if (aliases[key] && aliases[key] in record) return record[aliases[key]];
- if (['front', 'back', 'handheld'].includes(key)) {
- try {
- const pictures = JSON.parse(String(record['IdCardPics'] ?? '[]')) as Array<{ url?: string }>;
- return pictures[['front', 'back', 'handheld'].indexOf(key)]?.url ?? '';
- } catch { return ''; }
- }
- if (key === 'proId') {
- const extra = this.asRecord(addon);
- const carts = Array.isArray(extra['cart']) ? extra['cart'] as Record<string, unknown>[] : [];
- return carts[0]?.['ProID'] ?? '';
- }
- if (this.meta.path === 'pages/member/acc_destory') {
- if (key === 'concat') return String(record['PubContent'] ?? '').replace(/^联系方式[::]/, '');
- if (key === 'remark') return record['PubRemind'] ?? '';
- }
- if (key === 'pics' && record['pics']) {
- try {
- const pictures = JSON.parse(String(record['pics'])) as Array<{ url?: string; name?: string; desc?: string; link?: string }>;
- return JSON.stringify(pictures);
- } catch { return ''; }
- }
- return undefined;
- }
- private loadProductNodes(): void {
- this.api.get<unknown>('node_list', { pid: 2 }).subscribe({
- next: ({ result }) => this.nodeOptions.set((Array.isArray(result) ? result : [])
- .filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === 'object' && Number((item as Record<string, unknown>)['NodeID']) !== 19)
- .map((item) => ({ label: String(item['NodeName'] ?? item['NodeID']), value: Number(item['NodeID']) }))),
- error: (error: ApiError) => this.platform.toast(`商品类别加载失败:${error.message}`, 'error'),
- });
- }
- private prepareOrderProducts(addon: unknown): void {
- if (this.meta.path !== 'pages/shop/order_comment_add') return;
- const extra = this.asRecord(addon);
- const carts = Array.isArray(extra['cart']) ? extra['cart'] as Array<Record<string, unknown>> : [];
- this.productOptions.set(carts.map((item) => ({ label: this.productName(item), value: String(item['ProID'] ?? '') })).filter((item) => item.value));
- if (carts.length === 1) this.values['proId'] = String(carts[0]['ProID'] ?? '');
- }
- private loadExistingAppeal(order: Record<string, unknown>): void {
- const params = { cid: order['id'] ?? order['ID'], uid: this.session.user()?.userId };
- const apply = (item: Record<string, unknown>, pubid: number) => {
- this.appealAudit.set(item);
- this.values['type'] = pubid;
- this.values['concat'] = String(item['Concat'] ?? '');
- this.values['reason'] = String(item['PubContent'] ?? '');
- this.values['images'] = String(item['Pics'] ?? '');
- };
- this.api.get<unknown>('pub_list', { ...params, pubid: 10 }).subscribe({
- next: ({ result }) => {
- const returned = Array.isArray(result) ? this.asRecord(result[0]) : {};
- if (returned['ID']) { apply(returned, 10); return; }
- this.api.get<unknown>('pub_list', { ...params, pubid: 11 }).subscribe({
- next: ({ result: exchange }) => { const item = Array.isArray(exchange) ? this.asRecord(exchange[0]) : {}; if (item['ID']) apply(item, 11); },
- error: () => undefined,
- });
- },
- error: () => undefined,
- });
- }
- private parseObject(value: unknown): Record<string, unknown> {
- try { return this.asRecord(JSON.parse(String(value ?? '{}'))); } catch { return {}; }
- }
- private asRecord(value: unknown): Record<string, unknown> {
- return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
- }
- }
|