resource-page.component.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import { DropdownDirective } from '../../shared/dropdown.directive';
  2. import { CommonModule } from '@angular/common';
  3. import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';
  4. import { FormsModule } from '@angular/forms';
  5. import { ActivatedRoute, Router } from '@angular/router';
  6. import { AlertCircle, ChevronRight, Inbox, LucideAngularModule, Plus, RefreshCw, Send, Trash2, X } from 'lucide-angular';
  7. import { finalize } from 'rxjs';
  8. import { ApiError, ApiService } from '../../core/api.service';
  9. import { actionMatches, buildActionPayload, buildItemNavigation, buildLoadParams, itemId } from '../../core/business-rules';
  10. import { PageAction, PageMeta } from '../../core/models';
  11. import { PlatformService } from '../../core/platform.service';
  12. import { SessionService } from '../../core/session.service';
  13. type DataItem = Record<string, unknown>;
  14. @Component({
  15. selector: 'app-resource-page',
  16. standalone: true,
  17. imports: [DropdownDirective, CommonModule, FormsModule, LucideAngularModule],
  18. templateUrl: './resource-page.component.html',
  19. styleUrl: './resource-page.component.scss',
  20. changeDetection: ChangeDetectionStrategy.OnPush,
  21. })
  22. export class ResourcePageComponent implements OnInit {
  23. readonly meta = inject(ActivatedRoute).snapshot.data['meta'] as PageMeta;
  24. readonly items = signal<DataItem[]>([]);
  25. readonly detail = signal<DataItem | null>(null);
  26. readonly error = signal('');
  27. readonly loading = signal(false);
  28. readonly activeTab = signal(0);
  29. readonly selectedAction = signal<PageAction | null>(null);
  30. readonly selectedItem = signal<DataItem | null>(null);
  31. readonly contextOrder = signal<DataItem | null>(null);
  32. readonly contextCarts = signal<DataItem[]>([]);
  33. readonly icons = { AlertCircle, ChevronRight, Inbox, Plus, RefreshCw, Send, Trash2, X };
  34. actionValues: Record<string, unknown> = {};
  35. private readonly route = inject(ActivatedRoute);
  36. private readonly router = inject(Router);
  37. private readonly api = inject(ApiService);
  38. private readonly session = inject(SessionService);
  39. private readonly platform = inject(PlatformService);
  40. ngOnInit(): void {
  41. if (this.meta.path === 'pages/shop/order_comment') this.loadOrderContext();
  42. else this.load();
  43. }
  44. load(): void {
  45. if (!this.meta.api) return;
  46. this.loading.set(true);
  47. this.error.set('');
  48. const user = this.session.user();
  49. const action = this.meta.tabActions?.[this.activeTab()] ?? this.meta.api.action;
  50. const binding = { ...this.meta.api, action };
  51. const params = buildLoadParams(this.meta, binding, this.route.snapshot.queryParams, user, this.activeTab());
  52. const request = binding.method === 'post'
  53. ? this.api.post<unknown>(action, params)
  54. : this.api.get<unknown>(action, params);
  55. request.pipe(finalize(() => this.loading.set(false))).subscribe({
  56. next: (response) => {
  57. const value = this.pickPath(response.result, this.meta.api?.resultPath);
  58. const list = this.attachAddon(this.asList(value), response.addon);
  59. this.items.set(this.meta.path === 'pages/member/ranklist' ? list.map((item, index) => ({ ...item, index: index + 1 })) : list);
  60. this.detail.set(list.length ? null : this.asObject(value));
  61. },
  62. error: (error: ApiError) => {
  63. if (this.meta.path === 'pages/member/ranklist' && error.message.includes('接口不存在')) {
  64. this.items.set([]);
  65. this.error.set('');
  66. return;
  67. }
  68. this.error.set(error.message);
  69. },
  70. });
  71. for (const binding of this.meta.onLoadActions ?? []) {
  72. const extraPayload = buildLoadParams(this.meta, binding, this.route.snapshot.queryParams, user, this.activeTab());
  73. if (binding.action === 'node_get' && !extraPayload['id']) extraPayload['id'] = extraPayload['nid'] ?? extraPayload['pnid'];
  74. const extra = binding.method === 'post' ? this.api.post(binding.action, extraPayload) : this.api.get(binding.action, extraPayload);
  75. extra.subscribe({ error: () => undefined });
  76. }
  77. }
  78. selectTab(index: number): void {
  79. this.activeTab.set(index);
  80. this.load();
  81. }
  82. open(item: DataItem): void {
  83. if (this.meta.path === 'pages/member/ranklist' && this.activeTab() === 0) return;
  84. const target = buildItemNavigation(this.meta, item);
  85. if (target) void this.router.navigate([target.path], { queryParams: target.queryParams });
  86. }
  87. addRoute(): string | null {
  88. return ({
  89. 'pages/member/purse_outacc': '/pages/member/purse_outacc_add',
  90. 'pages/member/receaddr': '/pages/member/receaddr_add',
  91. 'pages/member/invoice': '/pages/member/invoice_add',
  92. 'pages/member/myshop': '/pages/member/myshop_add',
  93. 'pages/shop/order_comment': '/pages/shop/order_comment_add',
  94. } as Record<string, string>)[this.meta.path] ?? null;
  95. }
  96. create(): void {
  97. const target = this.addRoute();
  98. const queryParams = this.meta.path === 'pages/shop/order_comment' ? { id: this.route.snapshot.queryParamMap.get('id') } : {};
  99. if (target) void this.router.navigate([target], { queryParams });
  100. }
  101. async remove(item: DataItem, event: Event): Promise<void> {
  102. event.stopPropagation();
  103. if (!this.meta.deleteAction || !(await this.platform.confirm('确认删除这条记录?'))) return;
  104. const action: PageAction = { label: '删除', action: this.meta.deleteAction, scope: 'item', success: 'remove-item' };
  105. const payload = buildActionPayload(this.meta, action, this.route.snapshot.queryParams, this.session.user(), {}, item);
  106. this.platform.loading.set(true);
  107. this.api.post(this.meta.deleteAction, payload).pipe(finalize(() => this.platform.loading.set(false))).subscribe({
  108. next: () => {
  109. this.platform.toast('删除成功', 'success');
  110. this.items.update((items) => items.filter((entry) => entry !== item));
  111. },
  112. error: (error: ApiError) => this.platform.toast(error.message, 'error'),
  113. });
  114. }
  115. beginAction(action: PageAction, item: DataItem | null = null, event?: Event): void {
  116. event?.stopPropagation();
  117. this.actionValues = {};
  118. this.selectedItem.set(item);
  119. this.selectedAction.set(action);
  120. if (!action.fields?.length) void this.submitAction(action);
  121. }
  122. closeAction(): void {
  123. this.selectedAction.set(null);
  124. this.selectedItem.set(null);
  125. this.actionValues = {};
  126. }
  127. async submitAction(action = this.selectedAction()): Promise<void> {
  128. if (!action) return;
  129. if (action.confirm && !(await this.platform.confirm(action.confirm))) return;
  130. for (const field of action.fields ?? []) {
  131. if (field.required && !String(this.actionValues[field.key] ?? '').trim()) {
  132. this.platform.toast(`请填写${field.label}`, 'error');
  133. return;
  134. }
  135. }
  136. const item = this.selectedItem();
  137. const payload = buildActionPayload(this.meta, action, this.route.snapshot.queryParams, this.session.user(), this.actionValues, item);
  138. this.platform.loading.set(true);
  139. const request = action.method === 'get' ? this.api.get(action.action, payload) : this.api.post(action.action, payload);
  140. request.pipe(finalize(() => this.platform.loading.set(false))).subscribe({
  141. next: () => {
  142. this.platform.toast(`${action.label}成功`, 'success');
  143. if (action.success === 'remove-item' && item) this.items.update((items) => items.filter((entry) => entry !== item));
  144. this.closeAction();
  145. if (action.success !== 'none' && action.success !== 'remove-item') this.load();
  146. },
  147. error: (error: ApiError) => this.platform.toast(error.message, 'error'),
  148. });
  149. }
  150. titleOf(item: DataItem): string {
  151. return String(item['Title'] ?? item['Proname'] ?? item['OrderNo'] ?? item['ArriveName'] ?? item['InvHead'] ?? item['ReceivName'] ?? item['PeopleName'] ?? item['title'] ?? item['name'] ?? item['HoneyName'] ?? item['UserName'] ?? `记录 ${this.itemId(item) || ''}`);
  152. }
  153. bodyOf(item: DataItem): string { return String(item['MsgContent'] ?? item['PubContent'] ?? ''); }
  154. scoreOf(item: DataItem): number { return Math.max(0, Math.min(5, Number(item['Score'] ?? 0))); }
  155. contextProduct(item: DataItem): DataItem { return this.asObject(item['_product']) ?? {}; }
  156. contextProductName(item: DataItem): string { return String(item['Proname'] ?? this.parseObject(item['ProInfo'])['Proname'] ?? '商品'); }
  157. contextProductImage(item: DataItem): string { return this.api.assetUrl(item['proimg'] ?? this.parseObject(item['ProInfo'])['Thumbnails']); }
  158. subtitleOf(item: DataItem): string {
  159. if (this.meta.path === 'pages/member/ranklist') {
  160. return this.activeTab() === 0
  161. ? `团队规模:${item['num'] ?? 0} · 第${item['index'] ?? '-'}名`
  162. : `${this.activeTab() === 4 ? '回购数量' : '销量'}:${item['num'] ?? 0} · 第${item['index'] ?? '-'}名`;
  163. }
  164. return String(item['Abstract'] ?? item['synopsis'] ?? item['Remind'] ?? item['AddTime'] ?? item['CDate'] ?? item['Status'] ?? item['NodeName'] ?? '').replace(/<[^>]*>/g, '').slice(0, 120);
  165. }
  166. imageOf(item: DataItem): string | null {
  167. const carts = Array.isArray(item['cart']) ? item['cart'] as DataItem[] : [];
  168. const productInfo = this.parseObject(carts[0]?.['ProInfo']);
  169. const linkedProduct = this.contextProduct(item);
  170. const raw = item['TopImg'] ?? item['Thumbnails'] ?? item['Pic'] ?? item['UserFace'] ?? item['userFace'] ?? item['Image'] ?? linkedProduct['proimg'] ?? this.parseObject(linkedProduct['ProInfo'])['Thumbnails'] ?? carts[0]?.['proimg'] ?? productInfo['Thumbnails'];
  171. return raw ? this.api.assetUrl(raw) : null;
  172. }
  173. itemId(item: DataItem): string | number | null { return itemId(item); }
  174. itemActions(item: DataItem): PageAction[] {
  175. return (this.meta.actions ?? []).filter((action) => {
  176. if (action.scope !== 'item' || !actionMatches(action, item)) return false;
  177. if (this.meta.path === 'pages/member/myshop' && action.key === 'stop-sale') return this.activeTab() === 0;
  178. if (this.meta.path === 'pages/member/myshop' && action.key === 'start-sale') return this.activeTab() === 2;
  179. return true;
  180. });
  181. }
  182. detailEntries(): Array<{ key: string; value: string }> {
  183. const detail = this.detail();
  184. if (!detail) return [];
  185. return Object.entries(detail)
  186. .filter(([, value]) => value !== null && value !== undefined && typeof value !== 'object')
  187. .slice(0, 24)
  188. .map(([key, value]) => ({ key, value: String(value) }));
  189. }
  190. private asList(value: unknown): DataItem[] {
  191. if (Array.isArray(value)) return value.filter(this.isObject);
  192. if (!this.isObject(value)) return [];
  193. for (const key of ['list', 'items', 'data', 'CartDT', 'OrderList', 'Result', 'rows']) {
  194. const nested = value[key];
  195. if (Array.isArray(nested)) return nested.filter(this.isObject);
  196. }
  197. for (const nested of Object.values(value)) if (Array.isArray(nested)) return nested.filter(this.isObject);
  198. return [];
  199. }
  200. private asObject(value: unknown): DataItem | null { return this.isObject(value) ? value : null; }
  201. private isObject(value: unknown): value is DataItem { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); }
  202. private pickPath(value: unknown, path?: string): unknown {
  203. if (!path) return value;
  204. return path.split('.').reduce<unknown>((current, key) => this.isObject(current) ? current[key] : undefined, value);
  205. }
  206. private attachAddon(list: DataItem[], addon: unknown): DataItem[] {
  207. if (this.meta.path === 'pages/member/myshop_order' && Array.isArray(addon)) {
  208. return list.map((order) => ({
  209. ...order,
  210. cart: addon.filter((entry) => this.isObject(entry) && String(entry['ID']) === String(order['ID'])),
  211. }));
  212. }
  213. if (this.meta.path === 'pages/shop/order_comment') {
  214. return list.map((comment) => ({ ...comment, _product: this.contextCarts().find((cart) => String(cart['ProID']) === String(comment['ProID'])) }));
  215. }
  216. return list;
  217. }
  218. private loadOrderContext(): void {
  219. const id = this.route.snapshot.queryParamMap.get('id');
  220. if (!id) { this.error.set('缺少订单编号'); return; }
  221. this.loading.set(true);
  222. this.api.get<unknown>('order_get', { id, uid: this.session.user()?.userId }).subscribe({
  223. next: ({ result, addon }) => {
  224. this.contextOrder.set(this.asObject(result));
  225. const extra = this.asObject(addon) ?? {};
  226. const carts = Array.isArray(extra['cart']) ? extra['cart'] as DataItem[] : [];
  227. this.contextCarts.set(carts.map((item) => {
  228. const product = this.parseObject(item['ProInfo']);
  229. return { ...product, ...item, proimg: item['proimg'] ?? product['Thumbnails'] };
  230. }));
  231. this.load();
  232. },
  233. error: (error: ApiError) => { this.loading.set(false); this.error.set(`订单加载失败:${error.message}`); },
  234. });
  235. }
  236. private parseObject(value: unknown): DataItem {
  237. if (this.isObject(value)) return value;
  238. try { return JSON.parse(String(value ?? '{}')) as DataItem; } catch { return {}; }
  239. }
  240. }